@indigoai-us/hq-cli 5.59.0 → 5.60.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/dist/commands/meetings.js +127 -14
- package/dist/commands/secrets.js +7 -3
- package/package.json +1 -1
- package/src/commands/meetings.test.ts +116 -2
- package/src/commands/meetings.ts +206 -37
- package/src/commands/secrets.test.ts +89 -0
- package/src/commands/secrets.ts +5 -1
|
@@ -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]="6bb33e65-134a-5b0a-917b-ca084d4c8366")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
@@ -13,6 +13,17 @@ function formatDuration(seconds) {
|
|
|
13
13
|
return `${m}m ${s}s`;
|
|
14
14
|
return `${s}s`;
|
|
15
15
|
}
|
|
16
|
+
function safeFormatDuration(seconds) {
|
|
17
|
+
return typeof seconds === "number" && Number.isFinite(seconds) ? formatDuration(seconds) : "-";
|
|
18
|
+
}
|
|
19
|
+
function safeFormatDate(iso, options) {
|
|
20
|
+
if (typeof iso !== "string" || iso.length === 0)
|
|
21
|
+
return "-";
|
|
22
|
+
const date = new Date(iso);
|
|
23
|
+
if (Number.isNaN(date.getTime()))
|
|
24
|
+
return "-";
|
|
25
|
+
return options ? date.toLocaleString("en-US", options) : date.toLocaleString();
|
|
26
|
+
}
|
|
16
27
|
function formatTimestamp(ts) {
|
|
17
28
|
const m = Math.floor(ts / 60);
|
|
18
29
|
const s = Math.floor(ts % 60);
|
|
@@ -32,6 +43,38 @@ function statusBadge(status) {
|
|
|
32
43
|
return chalk.dim(status);
|
|
33
44
|
}
|
|
34
45
|
}
|
|
46
|
+
function isMarkdownShape(x) {
|
|
47
|
+
if (!x || typeof x !== "object")
|
|
48
|
+
return false;
|
|
49
|
+
const candidate = x;
|
|
50
|
+
return candidate.sourceShape === "markdown" || Boolean(candidate.source?.frontmatter);
|
|
51
|
+
}
|
|
52
|
+
function hasSignals(signals) {
|
|
53
|
+
return Boolean(signals &&
|
|
54
|
+
typeof signals === "object" &&
|
|
55
|
+
!Array.isArray(signals) &&
|
|
56
|
+
Object.keys(signals).length > 0);
|
|
57
|
+
}
|
|
58
|
+
function renderSignals(signals) {
|
|
59
|
+
console.log(chalk.bold("Signals"));
|
|
60
|
+
for (const [key, value] of Object.entries(signals)) {
|
|
61
|
+
if (value === null || value === undefined) {
|
|
62
|
+
console.log(` ${key}: -`);
|
|
63
|
+
}
|
|
64
|
+
else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
65
|
+
console.log(` ${key}: ${String(value)}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const rendered = JSON.stringify(value, null, 2)
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map((line) => ` ${line}`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
console.log(` ${key}:`);
|
|
73
|
+
console.log(rendered);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
}
|
|
35
78
|
async function resolveShortId(token, prefix, query) {
|
|
36
79
|
if (prefix.includes("-") && prefix.length > 8)
|
|
37
80
|
return prefix;
|
|
@@ -99,25 +142,28 @@ function printMeetingTable(meetings) {
|
|
|
99
142
|
const id = m.meetingId.slice(0, 8);
|
|
100
143
|
const fullTitle = displayTitle(m);
|
|
101
144
|
const title = fullTitle.length > TITLE_W ? fullTitle.slice(0, TITLE_W - 1) + "…" : fullTitle;
|
|
102
|
-
const date =
|
|
145
|
+
const date = safeFormatDate(m.startTime, {
|
|
103
146
|
month: "short",
|
|
104
147
|
day: "numeric",
|
|
105
148
|
hour: "2-digit",
|
|
106
149
|
minute: "2-digit",
|
|
107
150
|
});
|
|
108
|
-
const dur =
|
|
151
|
+
const dur = safeFormatDuration(m.duration);
|
|
152
|
+
const status = m.status ? statusBadge(m.status) : chalk.dim("-");
|
|
153
|
+
const parts = typeof m.participantCount === "number" ? String(m.participantCount) : "-";
|
|
109
154
|
const flags = [
|
|
110
155
|
m.hasTranscript ? "T" : "",
|
|
111
156
|
m.hasNotes ? "N" : "",
|
|
157
|
+
m.hasSignals ? "S" : "",
|
|
112
158
|
].filter(Boolean).join("") || "-";
|
|
113
159
|
console.log([
|
|
114
160
|
chalk.cyan(id.padEnd(ID_W)),
|
|
115
161
|
title.padEnd(TITLE_W),
|
|
116
162
|
chalk.dim(date.padEnd(DATE_W)),
|
|
117
163
|
dur.padEnd(DUR_W),
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
flags,
|
|
164
|
+
status.padEnd(STATUS_W + 10), // chalk adds escape chars
|
|
165
|
+
parts.padEnd(PARTS_W),
|
|
166
|
+
flags.padEnd(FLAGS_W),
|
|
121
167
|
].join(" "));
|
|
122
168
|
}
|
|
123
169
|
}
|
|
@@ -188,16 +234,34 @@ export function registerMeetingsCommand(program) {
|
|
|
188
234
|
console.log(JSON.stringify(detail, null, 2));
|
|
189
235
|
return;
|
|
190
236
|
}
|
|
191
|
-
|
|
237
|
+
if (isMarkdownShape(detail)) {
|
|
238
|
+
const fm = detail.source.frontmatter ?? {};
|
|
239
|
+
console.log(chalk.bold(`\n${fm.title || "(untitled)"}\n`));
|
|
240
|
+
console.log(` ID: ${chalk.cyan(detail.meetingId)}`);
|
|
241
|
+
console.log(` Status: ${fm.bot_status || "-"}`);
|
|
242
|
+
console.log(` Date: ${safeFormatDate(fm.scheduled_start_time || fm.created_at)}`);
|
|
243
|
+
console.log(` Platform: ${fm.meeting_platform || "-"}`);
|
|
244
|
+
console.log(` Origin: ${fm.origin || "-"}`);
|
|
245
|
+
console.log(` Company: ${fm.company_id || "-"}`);
|
|
246
|
+
if (fm.meeting_url)
|
|
247
|
+
console.log(` Meeting URL: ${fm.meeting_url}`);
|
|
248
|
+
if (hasSignals(detail.signals)) {
|
|
249
|
+
console.log(` Signals: ${Object.keys(detail.signals).length}`);
|
|
250
|
+
}
|
|
251
|
+
console.log(chalk.dim(`\n This meeting is stored as a markdown document. Use \`hq meetings transcript ${detail.meetingId.slice(0, 8)}\` to view the full document.`));
|
|
252
|
+
console.log();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
console.log(chalk.bold(`\n${detail.title ?? "(untitled)"}\n`));
|
|
192
256
|
console.log(` ID: ${chalk.cyan(detail.meetingId)}`);
|
|
193
|
-
console.log(` Status: ${statusBadge(detail.status)}`);
|
|
194
|
-
console.log(` Date: ${
|
|
195
|
-
console.log(` Duration: ${
|
|
196
|
-
console.log(` Source: ${detail.sourceApp} (${detail.botProvider})`);
|
|
257
|
+
console.log(` Status: ${detail.status ? statusBadge(detail.status) : chalk.dim("-")}`);
|
|
258
|
+
console.log(` Date: ${safeFormatDate(detail.startTime)}`);
|
|
259
|
+
console.log(` Duration: ${safeFormatDuration(detail.duration)}`);
|
|
260
|
+
console.log(` Source: ${detail.sourceApp ?? "-"} (${detail.botProvider ?? "-"})`);
|
|
197
261
|
console.log(` Shared: ${detail.isShared ? "yes" : "no"}`);
|
|
198
|
-
if (detail.participants
|
|
262
|
+
if ((detail.participants?.length ?? 0) > 0) {
|
|
199
263
|
console.log(chalk.bold("\n Participants:"));
|
|
200
|
-
for (const p of detail.participants) {
|
|
264
|
+
for (const p of detail.participants ?? []) {
|
|
201
265
|
const name = p.name ?? p.email;
|
|
202
266
|
const role = p.role === "organizer" ? chalk.yellow(" (organizer)") : "";
|
|
203
267
|
console.log(` - ${name}${role}`);
|
|
@@ -325,6 +389,26 @@ export function registerMeetingsCommand(program) {
|
|
|
325
389
|
if (!res.ok)
|
|
326
390
|
await handleApiError(res);
|
|
327
391
|
const detail = (await res.json());
|
|
392
|
+
if (isMarkdownShape(detail)) {
|
|
393
|
+
const documentUrl = detail.source.presigned_url;
|
|
394
|
+
if (!documentUrl) {
|
|
395
|
+
console.error(chalk.red("No document available for this meeting."));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
const docRes = await fetch(documentUrl);
|
|
399
|
+
if (!docRes.ok) {
|
|
400
|
+
console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const markdown = await docRes.text();
|
|
404
|
+
if (meetings.opts().json) {
|
|
405
|
+
console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
console.log(chalk.bold(`\nTranscript: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
409
|
+
console.log(markdown);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
328
412
|
if (!detail.documentUrl) {
|
|
329
413
|
console.error(chalk.red("No document URL available for this meeting."));
|
|
330
414
|
process.exit(1);
|
|
@@ -375,6 +459,35 @@ export function registerMeetingsCommand(program) {
|
|
|
375
459
|
if (!res.ok)
|
|
376
460
|
await handleApiError(res);
|
|
377
461
|
const detail = (await res.json());
|
|
462
|
+
if (isMarkdownShape(detail)) {
|
|
463
|
+
if (hasSignals(detail.signals)) {
|
|
464
|
+
if (meetings.opts().json) {
|
|
465
|
+
console.log(JSON.stringify(detail.signals, null, 2));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(chalk.bold(`\nMeeting Notes: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
469
|
+
renderSignals(detail.signals);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const documentUrl = detail.source.presigned_url;
|
|
473
|
+
if (!documentUrl) {
|
|
474
|
+
console.log(chalk.yellow("No notes available for this meeting."));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const docRes = await fetch(documentUrl);
|
|
478
|
+
if (!docRes.ok) {
|
|
479
|
+
console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
const markdown = await docRes.text();
|
|
483
|
+
if (meetings.opts().json) {
|
|
484
|
+
console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.log(chalk.bold(`\nMeeting Notes: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
488
|
+
console.log(markdown);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
378
491
|
if (!detail.documentUrl) {
|
|
379
492
|
console.error(chalk.red("No document URL available for this meeting."));
|
|
380
493
|
process.exit(1);
|
|
@@ -437,4 +550,4 @@ export function registerMeetingsCommand(program) {
|
|
|
437
550
|
});
|
|
438
551
|
}
|
|
439
552
|
//# sourceMappingURL=meetings.js.map
|
|
440
|
-
//# debugId=
|
|
553
|
+
//# debugId=6bb33e65-134a-5b0a-917b-ca084d4c8366
|
package/dist/commands/secrets.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]="3b78d543-70d6-52e2-b19f-fa2650534e97")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import * as readline from "node:readline";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -218,7 +218,11 @@ export function scrubSandboxOutput(text, secretNames = []) {
|
|
|
218
218
|
}
|
|
219
219
|
function renderSandboxJobResult(job, secretNames) {
|
|
220
220
|
if (job.output) {
|
|
221
|
-
|
|
221
|
+
const output = scrubSandboxOutput(job.output, secretNames);
|
|
222
|
+
process.stdout.write(output);
|
|
223
|
+
if (output.length > 0 && !output.endsWith("\n")) {
|
|
224
|
+
process.stdout.write("\n");
|
|
225
|
+
}
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
function normalizePolicyRecord(secretPath, data) {
|
|
@@ -1254,4 +1258,4 @@ export function registerSecretsCommand(program) {
|
|
|
1254
1258
|
});
|
|
1255
1259
|
}
|
|
1256
1260
|
//# sourceMappingURL=secrets.js.map
|
|
1257
|
-
//# debugId=
|
|
1261
|
+
//# debugId=3b78d543-70d6-52e2-b19f-fa2650534e97
|
package/package.json
CHANGED
|
@@ -45,6 +45,7 @@ beforeEach(() => {
|
|
|
45
45
|
|
|
46
46
|
afterEach(() => {
|
|
47
47
|
vi.restoreAllMocks();
|
|
48
|
+
vi.unstubAllGlobals();
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
function buildProgram(): Command {
|
|
@@ -298,7 +299,7 @@ describe("meetings set-company", () => {
|
|
|
298
299
|
"acme",
|
|
299
300
|
]);
|
|
300
301
|
|
|
301
|
-
const output = logSpy.mock.calls.map(([line]) => String(line)).join("\n");
|
|
302
|
+
const output = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
302
303
|
expect(output).not.toContain("Applied to 1 meetings");
|
|
303
304
|
expect(output).not.toContain("Refiled 0 transcripts");
|
|
304
305
|
expect(output).not.toContain("Future occurrences of this recurring series will inherit this attribution.");
|
|
@@ -345,8 +346,121 @@ describe("meetings list — null-safe rendering", () => {
|
|
|
345
346
|
program.parseAsync(["node", "hq", "meetings", "list", "--company", "indigo"]),
|
|
346
347
|
).resolves.toBeDefined();
|
|
347
348
|
|
|
348
|
-
const printed = logSpy.mock.calls.map(([line]) => String(line)).join("\n");
|
|
349
|
+
const printed = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
349
350
|
expect(printed).toContain("Meetings (2)");
|
|
350
351
|
expect(printed).toContain("(untitled)");
|
|
351
352
|
});
|
|
352
353
|
});
|
|
354
|
+
|
|
355
|
+
describe("meetings — markdown shape rendering", () => {
|
|
356
|
+
const markdownMeetingId = "589bfd78-aaaa-bbbb-cccc-1234567890ab";
|
|
357
|
+
const markdownUrl = "https://example.test/meeting.md";
|
|
358
|
+
|
|
359
|
+
function markdownDetail(signals: Record<string, unknown> = {}) {
|
|
360
|
+
return {
|
|
361
|
+
meetingId: markdownMeetingId,
|
|
362
|
+
sourceShape: "markdown",
|
|
363
|
+
source: {
|
|
364
|
+
path: "meetings/meeting.md",
|
|
365
|
+
presigned_url: markdownUrl,
|
|
366
|
+
frontmatter: {
|
|
367
|
+
title: "Markdown Standup",
|
|
368
|
+
channel: "eng",
|
|
369
|
+
origin: "recall",
|
|
370
|
+
company_id: "cmp_indigo",
|
|
371
|
+
meeting_url: "https://meet.example.test/standup",
|
|
372
|
+
meeting_platform: "google_meet",
|
|
373
|
+
calendar_event_id: null,
|
|
374
|
+
scheduled_start_time: "2026-02-03T15:30:00Z",
|
|
375
|
+
created_at: "2026-02-03T15:00:00Z",
|
|
376
|
+
updated_at: "2026-02-03T16:00:00Z",
|
|
377
|
+
ingested_at: "2026-02-03T16:05:00Z",
|
|
378
|
+
recall_bot_id: null,
|
|
379
|
+
bot_status: "done",
|
|
380
|
+
auto_scheduled: true,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
signals,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
it("lists markdown meetings without invalid legacy field output", async () => {
|
|
388
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
|
389
|
+
jsonRes({
|
|
390
|
+
meetings: [
|
|
391
|
+
{
|
|
392
|
+
meetingId: markdownMeetingId,
|
|
393
|
+
sourceShape: "markdown",
|
|
394
|
+
title: "Markdown Standup",
|
|
395
|
+
startTime: "2026-02-03T15:30:00Z",
|
|
396
|
+
channel: "eng",
|
|
397
|
+
ingested_at: "2026-02-03T16:05:00Z",
|
|
398
|
+
hasSignals: true,
|
|
399
|
+
companyId: "cmp_indigo",
|
|
400
|
+
attributed: true,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
}),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
const program = buildProgram();
|
|
407
|
+
await program.parseAsync(["node", "hq", "meetings", "list"]);
|
|
408
|
+
|
|
409
|
+
const output = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
410
|
+
expect(output).toContain("Markdown Standup");
|
|
411
|
+
expect(output).toContain(" S ");
|
|
412
|
+
expect(output).not.toContain("NaN");
|
|
413
|
+
expect(output).not.toContain("undefined");
|
|
414
|
+
expect(output).not.toContain("Invalid Date");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("gets markdown details from frontmatter without reading participants", async () => {
|
|
418
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes(markdownDetail({ action_items: ["Ship fix"] })));
|
|
419
|
+
|
|
420
|
+
const program = buildProgram();
|
|
421
|
+
await expect(
|
|
422
|
+
program.parseAsync(["node", "hq", "meetings", "get", markdownMeetingId]),
|
|
423
|
+
).resolves.toBeDefined();
|
|
424
|
+
|
|
425
|
+
const output = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
426
|
+
expect(output).toContain("Markdown Standup");
|
|
427
|
+
expect(output).toContain("Status: done");
|
|
428
|
+
expect(output).toContain("Platform: google_meet");
|
|
429
|
+
expect(output).toContain("Origin: recall");
|
|
430
|
+
expect(output).toContain("Company: cmp_indigo");
|
|
431
|
+
expect(output).toContain("Meeting URL: https://meet.example.test/standup");
|
|
432
|
+
expect(output).toContain("Signals: 1");
|
|
433
|
+
expect(output).toContain("stored as a markdown document");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("prints markdown transcript text from the presigned source URL", async () => {
|
|
437
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes(markdownDetail()));
|
|
438
|
+
const fetchMock = vi.fn(async () => new Response("# Markdown transcript\n\nHello team."));
|
|
439
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
440
|
+
|
|
441
|
+
const program = buildProgram();
|
|
442
|
+
await program.parseAsync(["node", "hq", "meetings", "transcript", markdownMeetingId]);
|
|
443
|
+
|
|
444
|
+
expect(fetchMock).toHaveBeenCalledWith(markdownUrl);
|
|
445
|
+
const output = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
446
|
+
expect(output).toContain("Transcript: Markdown Standup");
|
|
447
|
+
expect(output).toContain("# Markdown transcript");
|
|
448
|
+
expect(output).toContain("Hello team.");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("falls back to the markdown document for notes when markdown signals are empty", async () => {
|
|
452
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes(markdownDetail({})));
|
|
453
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response("# Notes\n\nFollow up next week.")));
|
|
454
|
+
|
|
455
|
+
const program = buildProgram();
|
|
456
|
+
await program.parseAsync(["node", "hq", "meetings", "notes", markdownMeetingId]);
|
|
457
|
+
|
|
458
|
+
const output = logSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
459
|
+
const errors = errSpy.mock.calls.map(([line]) => (line === undefined ? "" : String(line))).join("\n");
|
|
460
|
+
expect(output).toContain("Meeting Notes: Markdown Standup");
|
|
461
|
+
expect(output).toContain("# Notes");
|
|
462
|
+
expect(output).toContain("Follow up next week.");
|
|
463
|
+
expect(output).not.toContain("No document URL available");
|
|
464
|
+
expect(errors).not.toContain("No document URL available");
|
|
465
|
+
});
|
|
466
|
+
});
|
package/src/commands/meetings.ts
CHANGED
|
@@ -5,35 +5,67 @@ import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
|
5
5
|
|
|
6
6
|
interface MeetingListItem {
|
|
7
7
|
meetingId: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
sourceShape?: "markdown" | "json";
|
|
9
|
+
title?: string | null;
|
|
10
|
+
startTime?: string;
|
|
11
|
+
endTime?: string;
|
|
12
|
+
duration?: number;
|
|
13
|
+
participantCount?: number;
|
|
14
|
+
status?: string;
|
|
15
|
+
hasTranscript?: boolean;
|
|
16
|
+
hasNotes?: boolean;
|
|
17
|
+
channel?: string;
|
|
18
|
+
ingested_at?: string;
|
|
19
|
+
hasSignals?: boolean;
|
|
20
|
+
companyId?: string;
|
|
21
|
+
attributed?: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
interface MeetingDetail {
|
|
19
25
|
meetingId: string;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
sourceShape?: "markdown" | "json";
|
|
27
|
+
title?: string;
|
|
28
|
+
startTime?: string;
|
|
29
|
+
endTime?: string;
|
|
30
|
+
duration?: number;
|
|
31
|
+
participants?: Array<{ email: string; name: string | null; role: string }>;
|
|
32
|
+
calendarEventId?: string | null;
|
|
33
|
+
botProvider?: string;
|
|
34
|
+
sourceApp?: string;
|
|
35
|
+
companyId?: string;
|
|
36
|
+
status?: string;
|
|
37
|
+
recallBotId?: string | null;
|
|
38
|
+
isShared?: boolean;
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
updatedAt?: string;
|
|
41
|
+
documentUrl?: string;
|
|
42
|
+
hasTranscript?: boolean;
|
|
43
|
+
hasNotes?: boolean;
|
|
44
|
+
source?: MarkdownMeetingSource;
|
|
45
|
+
signals?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface MarkdownMeetingFrontmatter {
|
|
49
|
+
title?: string;
|
|
50
|
+
channel?: string;
|
|
51
|
+
origin?: string;
|
|
52
|
+
company_id?: string;
|
|
53
|
+
meeting_url?: string;
|
|
54
|
+
meeting_platform?: string;
|
|
55
|
+
calendar_event_id?: string | null;
|
|
56
|
+
scheduled_start_time?: string;
|
|
57
|
+
created_at?: string;
|
|
58
|
+
updated_at?: string;
|
|
59
|
+
ingested_at?: string;
|
|
60
|
+
recall_bot_id?: string | null;
|
|
61
|
+
bot_status?: string;
|
|
62
|
+
auto_scheduled?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface MarkdownMeetingSource {
|
|
66
|
+
path?: string;
|
|
67
|
+
presigned_url?: string;
|
|
68
|
+
frontmatter?: MarkdownMeetingFrontmatter;
|
|
37
69
|
}
|
|
38
70
|
|
|
39
71
|
interface TranscriptSegment {
|
|
@@ -78,6 +110,17 @@ function formatDuration(seconds: number): string {
|
|
|
78
110
|
return `${s}s`;
|
|
79
111
|
}
|
|
80
112
|
|
|
113
|
+
function safeFormatDuration(seconds: unknown): string {
|
|
114
|
+
return typeof seconds === "number" && Number.isFinite(seconds) ? formatDuration(seconds) : "-";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function safeFormatDate(iso: unknown, options?: Intl.DateTimeFormatOptions): string {
|
|
118
|
+
if (typeof iso !== "string" || iso.length === 0) return "-";
|
|
119
|
+
const date = new Date(iso);
|
|
120
|
+
if (Number.isNaN(date.getTime())) return "-";
|
|
121
|
+
return options ? date.toLocaleString("en-US", options) : date.toLocaleString();
|
|
122
|
+
}
|
|
123
|
+
|
|
81
124
|
function formatTimestamp(ts: number): string {
|
|
82
125
|
const m = Math.floor(ts / 60);
|
|
83
126
|
const s = Math.floor(ts % 60);
|
|
@@ -99,6 +142,45 @@ function statusBadge(status: string): string {
|
|
|
99
142
|
}
|
|
100
143
|
}
|
|
101
144
|
|
|
145
|
+
function isMarkdownShape(x: unknown): x is MeetingDetail & {
|
|
146
|
+
source: MarkdownMeetingSource;
|
|
147
|
+
} {
|
|
148
|
+
if (!x || typeof x !== "object") return false;
|
|
149
|
+
const candidate = x as {
|
|
150
|
+
sourceShape?: string;
|
|
151
|
+
source?: { frontmatter?: unknown };
|
|
152
|
+
};
|
|
153
|
+
return candidate.sourceShape === "markdown" || Boolean(candidate.source?.frontmatter);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function hasSignals(signals: unknown): signals is Record<string, unknown> {
|
|
157
|
+
return Boolean(
|
|
158
|
+
signals &&
|
|
159
|
+
typeof signals === "object" &&
|
|
160
|
+
!Array.isArray(signals) &&
|
|
161
|
+
Object.keys(signals).length > 0,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderSignals(signals: Record<string, unknown>): void {
|
|
166
|
+
console.log(chalk.bold("Signals"));
|
|
167
|
+
for (const [key, value] of Object.entries(signals)) {
|
|
168
|
+
if (value === null || value === undefined) {
|
|
169
|
+
console.log(` ${key}: -`);
|
|
170
|
+
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
171
|
+
console.log(` ${key}: ${String(value)}`);
|
|
172
|
+
} else {
|
|
173
|
+
const rendered = JSON.stringify(value, null, 2)
|
|
174
|
+
.split("\n")
|
|
175
|
+
.map((line) => ` ${line}`)
|
|
176
|
+
.join("\n");
|
|
177
|
+
console.log(` ${key}:`);
|
|
178
|
+
console.log(rendered);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
|
|
102
184
|
async function resolveShortId(
|
|
103
185
|
token: string,
|
|
104
186
|
prefix: string,
|
|
@@ -181,16 +263,19 @@ function printMeetingTable(meetings: MeetingListItem[]): void {
|
|
|
181
263
|
const id = m.meetingId.slice(0, 8);
|
|
182
264
|
const fullTitle = displayTitle(m);
|
|
183
265
|
const title = fullTitle.length > TITLE_W ? fullTitle.slice(0, TITLE_W - 1) + "…" : fullTitle;
|
|
184
|
-
const date =
|
|
266
|
+
const date = safeFormatDate(m.startTime, {
|
|
185
267
|
month: "short",
|
|
186
268
|
day: "numeric",
|
|
187
269
|
hour: "2-digit",
|
|
188
270
|
minute: "2-digit",
|
|
189
271
|
});
|
|
190
|
-
const dur =
|
|
272
|
+
const dur = safeFormatDuration(m.duration);
|
|
273
|
+
const status = m.status ? statusBadge(m.status) : chalk.dim("-");
|
|
274
|
+
const parts = typeof m.participantCount === "number" ? String(m.participantCount) : "-";
|
|
191
275
|
const flags = [
|
|
192
276
|
m.hasTranscript ? "T" : "",
|
|
193
277
|
m.hasNotes ? "N" : "",
|
|
278
|
+
m.hasSignals ? "S" : "",
|
|
194
279
|
].filter(Boolean).join("") || "-";
|
|
195
280
|
|
|
196
281
|
console.log(
|
|
@@ -199,9 +284,9 @@ function printMeetingTable(meetings: MeetingListItem[]): void {
|
|
|
199
284
|
title.padEnd(TITLE_W),
|
|
200
285
|
chalk.dim(date.padEnd(DATE_W)),
|
|
201
286
|
dur.padEnd(DUR_W),
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
flags,
|
|
287
|
+
status.padEnd(STATUS_W + 10), // chalk adds escape chars
|
|
288
|
+
parts.padEnd(PARTS_W),
|
|
289
|
+
flags.padEnd(FLAGS_W),
|
|
205
290
|
].join(" "),
|
|
206
291
|
);
|
|
207
292
|
}
|
|
@@ -286,17 +371,39 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
286
371
|
return;
|
|
287
372
|
}
|
|
288
373
|
|
|
289
|
-
|
|
374
|
+
if (isMarkdownShape(detail)) {
|
|
375
|
+
const fm = detail.source.frontmatter ?? {};
|
|
376
|
+
console.log(chalk.bold(`\n${fm.title || "(untitled)"}\n`));
|
|
377
|
+
console.log(` ID: ${chalk.cyan(detail.meetingId)}`);
|
|
378
|
+
console.log(` Status: ${fm.bot_status || "-"}`);
|
|
379
|
+
console.log(` Date: ${safeFormatDate(fm.scheduled_start_time || fm.created_at)}`);
|
|
380
|
+
console.log(` Platform: ${fm.meeting_platform || "-"}`);
|
|
381
|
+
console.log(` Origin: ${fm.origin || "-"}`);
|
|
382
|
+
console.log(` Company: ${fm.company_id || "-"}`);
|
|
383
|
+
if (fm.meeting_url) console.log(` Meeting URL: ${fm.meeting_url}`);
|
|
384
|
+
if (hasSignals(detail.signals)) {
|
|
385
|
+
console.log(` Signals: ${Object.keys(detail.signals).length}`);
|
|
386
|
+
}
|
|
387
|
+
console.log(
|
|
388
|
+
chalk.dim(
|
|
389
|
+
`\n This meeting is stored as a markdown document. Use \`hq meetings transcript ${detail.meetingId.slice(0, 8)}\` to view the full document.`,
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
console.log();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
console.log(chalk.bold(`\n${detail.title ?? "(untitled)"}\n`));
|
|
290
397
|
console.log(` ID: ${chalk.cyan(detail.meetingId)}`);
|
|
291
|
-
console.log(` Status: ${statusBadge(detail.status)}`);
|
|
292
|
-
console.log(` Date: ${
|
|
293
|
-
console.log(` Duration: ${
|
|
294
|
-
console.log(` Source: ${detail.sourceApp} (${detail.botProvider})`);
|
|
398
|
+
console.log(` Status: ${detail.status ? statusBadge(detail.status) : chalk.dim("-")}`);
|
|
399
|
+
console.log(` Date: ${safeFormatDate(detail.startTime)}`);
|
|
400
|
+
console.log(` Duration: ${safeFormatDuration(detail.duration)}`);
|
|
401
|
+
console.log(` Source: ${detail.sourceApp ?? "-"} (${detail.botProvider ?? "-"})`);
|
|
295
402
|
console.log(` Shared: ${detail.isShared ? "yes" : "no"}`);
|
|
296
403
|
|
|
297
|
-
if (detail.participants
|
|
404
|
+
if ((detail.participants?.length ?? 0) > 0) {
|
|
298
405
|
console.log(chalk.bold("\n Participants:"));
|
|
299
|
-
for (const p of detail.participants) {
|
|
406
|
+
for (const p of detail.participants ?? []) {
|
|
300
407
|
const name = p.name ?? p.email;
|
|
301
408
|
const role = p.role === "organizer" ? chalk.yellow(" (organizer)") : "";
|
|
302
409
|
console.log(` - ${name}${role}`);
|
|
@@ -447,6 +554,32 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
447
554
|
if (!res.ok) await handleApiError(res);
|
|
448
555
|
|
|
449
556
|
const detail = (await res.json()) as MeetingDetail;
|
|
557
|
+
|
|
558
|
+
if (isMarkdownShape(detail)) {
|
|
559
|
+
const documentUrl = detail.source.presigned_url;
|
|
560
|
+
if (!documentUrl) {
|
|
561
|
+
console.error(chalk.red("No document available for this meeting."));
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const docRes = await fetch(documentUrl);
|
|
566
|
+
if (!docRes.ok) {
|
|
567
|
+
console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const markdown = await docRes.text();
|
|
572
|
+
|
|
573
|
+
if (meetings.opts().json) {
|
|
574
|
+
console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
console.log(chalk.bold(`\nTranscript: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
579
|
+
console.log(markdown);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
450
583
|
if (!detail.documentUrl) {
|
|
451
584
|
console.error(chalk.red("No document URL available for this meeting."));
|
|
452
585
|
process.exit(1);
|
|
@@ -503,6 +636,42 @@ export function registerMeetingsCommand(program: Command): void {
|
|
|
503
636
|
if (!res.ok) await handleApiError(res);
|
|
504
637
|
|
|
505
638
|
const detail = (await res.json()) as MeetingDetail;
|
|
639
|
+
|
|
640
|
+
if (isMarkdownShape(detail)) {
|
|
641
|
+
if (hasSignals(detail.signals)) {
|
|
642
|
+
if (meetings.opts().json) {
|
|
643
|
+
console.log(JSON.stringify(detail.signals, null, 2));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
console.log(chalk.bold(`\nMeeting Notes: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
647
|
+
renderSignals(detail.signals);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const documentUrl = detail.source.presigned_url;
|
|
652
|
+
if (!documentUrl) {
|
|
653
|
+
console.log(chalk.yellow("No notes available for this meeting."));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const docRes = await fetch(documentUrl);
|
|
658
|
+
if (!docRes.ok) {
|
|
659
|
+
console.error(chalk.red(`Failed to download meeting document (${docRes.status})`));
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const markdown = await docRes.text();
|
|
664
|
+
|
|
665
|
+
if (meetings.opts().json) {
|
|
666
|
+
console.log(JSON.stringify({ meetingId: detail.meetingId, sourceShape: "markdown", markdown }, null, 2));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
console.log(chalk.bold(`\nMeeting Notes: ${detail.source.frontmatter?.title || "(untitled)"}\n`));
|
|
671
|
+
console.log(markdown);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
506
675
|
if (!detail.documentUrl) {
|
|
507
676
|
console.error(chalk.red("No document URL available for this meeting."));
|
|
508
677
|
process.exit(1);
|
|
@@ -264,6 +264,95 @@ describe("secrets sandbox", () => {
|
|
|
264
264
|
expect(stdoutSpy).toHaveBeenCalledWith("token [REDACTED]\nAPI_KEY=[REDACTED]\n");
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
it("terminates sandbox output with exactly one newline when output is present", async () => {
|
|
268
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
269
|
+
jobId: "job_123",
|
|
270
|
+
status: "succeeded",
|
|
271
|
+
output: "123",
|
|
272
|
+
exitCode: 0,
|
|
273
|
+
success: true,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const program = buildProgram();
|
|
277
|
+
await program.parseAsync([
|
|
278
|
+
"node",
|
|
279
|
+
"hq",
|
|
280
|
+
"secrets",
|
|
281
|
+
"sandbox",
|
|
282
|
+
"--only",
|
|
283
|
+
"API_KEY",
|
|
284
|
+
"--",
|
|
285
|
+
"my-skill",
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
expect(stdoutSpy.mock.calls.flat().join("")).toBe("123\n");
|
|
289
|
+
|
|
290
|
+
stdoutSpy.mockClear();
|
|
291
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
292
|
+
jobId: "job_123",
|
|
293
|
+
status: "succeeded",
|
|
294
|
+
output: "123\n",
|
|
295
|
+
exitCode: 0,
|
|
296
|
+
success: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await buildProgram().parseAsync([
|
|
300
|
+
"node",
|
|
301
|
+
"hq",
|
|
302
|
+
"secrets",
|
|
303
|
+
"sandbox",
|
|
304
|
+
"--only",
|
|
305
|
+
"API_KEY",
|
|
306
|
+
"--",
|
|
307
|
+
"my-skill",
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
expect(stdoutSpy.mock.calls.flat().join("")).toBe("123\n");
|
|
311
|
+
|
|
312
|
+
stdoutSpy.mockClear();
|
|
313
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
314
|
+
jobId: "job_123",
|
|
315
|
+
status: "succeeded",
|
|
316
|
+
output: "",
|
|
317
|
+
exitCode: 0,
|
|
318
|
+
success: true,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await buildProgram().parseAsync([
|
|
322
|
+
"node",
|
|
323
|
+
"hq",
|
|
324
|
+
"secrets",
|
|
325
|
+
"sandbox",
|
|
326
|
+
"--only",
|
|
327
|
+
"API_KEY",
|
|
328
|
+
"--",
|
|
329
|
+
"my-skill",
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
333
|
+
|
|
334
|
+
stdoutSpy.mockClear();
|
|
335
|
+
pollJobSpy.mockResolvedValueOnce({
|
|
336
|
+
jobId: "job_123",
|
|
337
|
+
status: "succeeded",
|
|
338
|
+
exitCode: 0,
|
|
339
|
+
success: true,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await buildProgram().parseAsync([
|
|
343
|
+
"node",
|
|
344
|
+
"hq",
|
|
345
|
+
"secrets",
|
|
346
|
+
"sandbox",
|
|
347
|
+
"--only",
|
|
348
|
+
"API_KEY",
|
|
349
|
+
"--",
|
|
350
|
+
"my-skill",
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
|
|
267
356
|
it("exits non-zero when the sandbox command fails", async () => {
|
|
268
357
|
pollJobSpy.mockResolvedValueOnce({
|
|
269
358
|
jobId: "job_123",
|
package/src/commands/secrets.ts
CHANGED
|
@@ -369,7 +369,11 @@ export function scrubSandboxOutput(text: string, secretNames: string[] = []): st
|
|
|
369
369
|
|
|
370
370
|
function renderSandboxJobResult(job: SandboxRunnerJob, secretNames: string[]): void {
|
|
371
371
|
if (job.output) {
|
|
372
|
-
|
|
372
|
+
const output = scrubSandboxOutput(job.output, secretNames);
|
|
373
|
+
process.stdout.write(output);
|
|
374
|
+
if (output.length > 0 && !output.endsWith("\n")) {
|
|
375
|
+
process.stdout.write("\n");
|
|
376
|
+
}
|
|
373
377
|
}
|
|
374
378
|
}
|
|
375
379
|
|