fireflies-api 0.5.1 → 0.6.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/cli/index.cjs +1435 -217
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1436 -222
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1372 -392
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +462 -3
- package/dist/index.d.ts +462 -3
- package/dist/index.js +1357 -393
- package/dist/index.js.map +1 -1
- package/dist/middleware/express.cjs +459 -16
- package/dist/middleware/express.cjs.map +1 -1
- package/dist/middleware/express.d.cts +2 -2
- package/dist/middleware/express.d.ts +2 -2
- package/dist/middleware/express.js +459 -16
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/fastify.cjs +459 -16
- package/dist/middleware/fastify.cjs.map +1 -1
- package/dist/middleware/fastify.d.cts +2 -2
- package/dist/middleware/fastify.d.ts +2 -2
- package/dist/middleware/fastify.js +459 -16
- package/dist/middleware/fastify.js.map +1 -1
- package/dist/middleware/hono.cjs +459 -16
- package/dist/middleware/hono.cjs.map +1 -1
- package/dist/middleware/hono.d.cts +2 -2
- package/dist/middleware/hono.d.ts +2 -2
- package/dist/middleware/hono.js +459 -16
- package/dist/middleware/hono.js.map +1 -1
- package/dist/templates/digest/compact.md +8 -0
- package/dist/templates/digest/default.md +44 -0
- package/dist/templates/digest/executive.md +22 -0
- package/dist/{types-CaHcwnKw.d.ts → types-BMzVSd6w.d.ts} +1 -1
- package/dist/{types-BX-3JcRI.d.cts → types-BeXRmVD7.d.cts} +1 -1
- package/dist/{types-DIPZmUl3.d.ts → types-D2XsCR5R.d.ts} +120 -1
- package/dist/{types-C_XxdRd1.d.cts → types-zVGqyFzP.d.cts} +120 -1
- package/package.json +6 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { io } from 'socket.io-client';
|
|
7
|
-
import
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
8
9
|
|
|
9
10
|
// src/cli/index.ts
|
|
10
11
|
|
|
@@ -282,8 +283,8 @@ async function retry(fn, options) {
|
|
|
282
283
|
if (attempt >= maxRetries || !shouldRetry(error, attempt)) {
|
|
283
284
|
throw error;
|
|
284
285
|
}
|
|
285
|
-
const
|
|
286
|
-
await sleep(
|
|
286
|
+
const delay2 = calculateDelay(error, attempt, baseDelay, maxDelay);
|
|
287
|
+
await sleep(delay2);
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
throw lastError;
|
|
@@ -432,9 +433,9 @@ var GraphQLClient = class {
|
|
|
432
433
|
if (!this.rateLimitTracker || !this.rateLimitConfig?.throttle) {
|
|
433
434
|
return;
|
|
434
435
|
}
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
await sleep2(
|
|
436
|
+
const delay2 = this.rateLimitTracker.getThrottleDelay(this.rateLimitConfig.throttle);
|
|
437
|
+
if (delay2 > 0) {
|
|
438
|
+
await sleep2(delay2);
|
|
438
439
|
}
|
|
439
440
|
}
|
|
440
441
|
/**
|
|
@@ -1232,6 +1233,46 @@ function getGroupKeyFn(groupBy2) {
|
|
|
1232
1233
|
}
|
|
1233
1234
|
}
|
|
1234
1235
|
|
|
1236
|
+
// src/helpers/batch.ts
|
|
1237
|
+
async function* batch(items, processor, options = {}) {
|
|
1238
|
+
const { delayMs = 100, handleRateLimit = true, maxRateLimitRetries = 3 } = options;
|
|
1239
|
+
let isFirst = true;
|
|
1240
|
+
for await (const item of items) {
|
|
1241
|
+
if (!isFirst && delayMs > 0) {
|
|
1242
|
+
await delay(delayMs);
|
|
1243
|
+
}
|
|
1244
|
+
isFirst = false;
|
|
1245
|
+
yield await processWithRetry(item, processor, {
|
|
1246
|
+
handleRateLimit,
|
|
1247
|
+
maxRateLimitRetries
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function processWithRetry(item, processor, options) {
|
|
1252
|
+
const { handleRateLimit, maxRateLimitRetries } = options;
|
|
1253
|
+
let retries = 0;
|
|
1254
|
+
while (true) {
|
|
1255
|
+
try {
|
|
1256
|
+
const result = await processor(item);
|
|
1257
|
+
return { item, result };
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
if (handleRateLimit && err instanceof RateLimitError && retries < maxRateLimitRetries) {
|
|
1260
|
+
const waitTime = err.retryAfter ?? 1e3;
|
|
1261
|
+
await delay(waitTime);
|
|
1262
|
+
retries++;
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
item,
|
|
1267
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function delay(ms) {
|
|
1273
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1235
1276
|
// src/helpers/domain-utils.ts
|
|
1236
1277
|
function extractDomain(email) {
|
|
1237
1278
|
const atIndex = email.indexOf("@");
|
|
@@ -1247,6 +1288,332 @@ function hasExternalParticipants(participants, internalDomain) {
|
|
|
1247
1288
|
});
|
|
1248
1289
|
}
|
|
1249
1290
|
|
|
1291
|
+
// src/helpers/markdown.ts
|
|
1292
|
+
var DEFAULT_OPTIONS2 = {
|
|
1293
|
+
includeMetadata: true,
|
|
1294
|
+
includeSummary: true,
|
|
1295
|
+
includeActionItems: true,
|
|
1296
|
+
actionItemFormat: "checkbox",
|
|
1297
|
+
includeTimestamps: false,
|
|
1298
|
+
speakerFormat: "bold",
|
|
1299
|
+
groupBySpeaker: true
|
|
1300
|
+
};
|
|
1301
|
+
async function transcriptToMarkdown(transcript, options = {}) {
|
|
1302
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
1303
|
+
const sections = [];
|
|
1304
|
+
if (opts.includeMetadata) {
|
|
1305
|
+
sections.push(formatMetadata(transcript));
|
|
1306
|
+
}
|
|
1307
|
+
if (opts.includeSummary && transcript.summary) {
|
|
1308
|
+
sections.push(formatSummary(transcript.summary, opts));
|
|
1309
|
+
}
|
|
1310
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1311
|
+
sections.push(formatTranscript(transcript.sentences, opts));
|
|
1312
|
+
}
|
|
1313
|
+
const content = sections.join("\n\n---\n\n");
|
|
1314
|
+
await writeIfOutputPath(content, options.outputPath);
|
|
1315
|
+
return content;
|
|
1316
|
+
}
|
|
1317
|
+
function formatMetadata(transcript) {
|
|
1318
|
+
const lines = [`# ${transcript.title || "Untitled Meeting"}`];
|
|
1319
|
+
if (transcript.dateString) {
|
|
1320
|
+
lines.push(`
|
|
1321
|
+
**Date:** ${formatDate(transcript.dateString)}`);
|
|
1322
|
+
}
|
|
1323
|
+
const duration = calculateDuration(transcript);
|
|
1324
|
+
if (duration > 0) {
|
|
1325
|
+
lines.push(`**Duration:** ${formatDuration(duration)}`);
|
|
1326
|
+
}
|
|
1327
|
+
const participants = getParticipantNames(transcript);
|
|
1328
|
+
if (participants.length > 0) {
|
|
1329
|
+
lines.push(`**Participants:** ${participants.join(", ")}`);
|
|
1330
|
+
}
|
|
1331
|
+
return lines.join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
function calculateDuration(transcript) {
|
|
1334
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1335
|
+
const lastSentence = transcript.sentences[transcript.sentences.length - 1];
|
|
1336
|
+
if (lastSentence) {
|
|
1337
|
+
return parseFloat(lastSentence.end_time);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return transcript.duration || 0;
|
|
1341
|
+
}
|
|
1342
|
+
function formatSummary(summary, opts) {
|
|
1343
|
+
const sections = ["## Summary"];
|
|
1344
|
+
if (summary.gist) {
|
|
1345
|
+
sections.push("", summary.gist);
|
|
1346
|
+
}
|
|
1347
|
+
if (summary.bullet_gist) {
|
|
1348
|
+
const bullets = parseMultilineField(summary.bullet_gist);
|
|
1349
|
+
if (bullets.length > 0) {
|
|
1350
|
+
sections.push("", "### Key Points");
|
|
1351
|
+
sections.push(bullets.map((p) => `- ${p}`).join("\n"));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
if (opts.includeActionItems && summary.action_items) {
|
|
1355
|
+
const items = parseMultilineField(summary.action_items);
|
|
1356
|
+
if (items.length > 0) {
|
|
1357
|
+
sections.push("", "### Action Items");
|
|
1358
|
+
const prefix = opts.actionItemFormat === "checkbox" ? "- [ ] " : "- ";
|
|
1359
|
+
sections.push(items.map((a) => `${prefix}${a}`).join("\n"));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return sections.join("\n");
|
|
1363
|
+
}
|
|
1364
|
+
function formatTranscript(sentences, opts) {
|
|
1365
|
+
const lines = ["## Transcript"];
|
|
1366
|
+
if (opts.groupBySpeaker) {
|
|
1367
|
+
const groups = groupSentencesBySpeaker(sentences);
|
|
1368
|
+
for (const group of groups) {
|
|
1369
|
+
lines.push("", formatSpeakerGroup(group, opts));
|
|
1370
|
+
}
|
|
1371
|
+
} else {
|
|
1372
|
+
for (const sentence of sentences) {
|
|
1373
|
+
lines.push("", formatSentence(sentence, opts));
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return lines.join("\n");
|
|
1377
|
+
}
|
|
1378
|
+
function groupSentencesBySpeaker(sentences) {
|
|
1379
|
+
const groups = [];
|
|
1380
|
+
let current = null;
|
|
1381
|
+
for (const sentence of sentences) {
|
|
1382
|
+
if (!current || current.speakerName !== sentence.speaker_name) {
|
|
1383
|
+
current = { speakerName: sentence.speaker_name, sentences: [] };
|
|
1384
|
+
groups.push(current);
|
|
1385
|
+
}
|
|
1386
|
+
current.sentences.push(sentence);
|
|
1387
|
+
}
|
|
1388
|
+
return groups;
|
|
1389
|
+
}
|
|
1390
|
+
function formatSpeakerGroup(group, opts) {
|
|
1391
|
+
const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
|
|
1392
|
+
const text = group.sentences.map((s) => s.text).join(" ");
|
|
1393
|
+
const firstSentence = group.sentences[0];
|
|
1394
|
+
if (opts.includeTimestamps && firstSentence) {
|
|
1395
|
+
const timestamp = formatTimestamp(firstSentence.start_time);
|
|
1396
|
+
return `${timestamp} ${speaker} ${text}`;
|
|
1397
|
+
}
|
|
1398
|
+
return `${speaker} ${text}`;
|
|
1399
|
+
}
|
|
1400
|
+
function formatSentence(sentence, opts) {
|
|
1401
|
+
const speaker = formatSpeakerName(sentence.speaker_name, opts.speakerFormat);
|
|
1402
|
+
if (opts.includeTimestamps) {
|
|
1403
|
+
const timestamp = formatTimestamp(sentence.start_time);
|
|
1404
|
+
return `${timestamp} ${speaker} ${sentence.text}`;
|
|
1405
|
+
}
|
|
1406
|
+
return `${speaker} ${sentence.text}`;
|
|
1407
|
+
}
|
|
1408
|
+
function formatSpeakerName(name, format) {
|
|
1409
|
+
switch (format) {
|
|
1410
|
+
case "bold":
|
|
1411
|
+
return `**${name}:**`;
|
|
1412
|
+
case "plain":
|
|
1413
|
+
return `${name}:`;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function formatTimestamp(startTime) {
|
|
1417
|
+
const seconds = parseFloat(startTime);
|
|
1418
|
+
const mins = Math.floor(seconds / 60);
|
|
1419
|
+
const secs = Math.floor(seconds % 60);
|
|
1420
|
+
return `[${mins}:${secs.toString().padStart(2, "0")}]`;
|
|
1421
|
+
}
|
|
1422
|
+
function formatDuration(seconds) {
|
|
1423
|
+
const hours = Math.floor(seconds / 3600);
|
|
1424
|
+
const mins = Math.floor(seconds % 3600 / 60);
|
|
1425
|
+
if (hours > 0) {
|
|
1426
|
+
return `${hours}h ${mins}m`;
|
|
1427
|
+
}
|
|
1428
|
+
return `${mins} minutes`;
|
|
1429
|
+
}
|
|
1430
|
+
function formatDate(isoString) {
|
|
1431
|
+
return new Date(isoString).toLocaleDateString("en-US", {
|
|
1432
|
+
weekday: "long",
|
|
1433
|
+
year: "numeric",
|
|
1434
|
+
month: "long",
|
|
1435
|
+
day: "numeric"
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
function getParticipantNames(transcript) {
|
|
1439
|
+
if (transcript.meeting_attendees?.length) {
|
|
1440
|
+
return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
|
|
1441
|
+
}
|
|
1442
|
+
return transcript.speakers?.map((s) => s.name) || [];
|
|
1443
|
+
}
|
|
1444
|
+
function parseMultilineField(value) {
|
|
1445
|
+
return value.split(/\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1446
|
+
}
|
|
1447
|
+
async function writeIfOutputPath(content, outputPath) {
|
|
1448
|
+
if (outputPath) {
|
|
1449
|
+
const { writeFile: writeFile3 } = await import('fs/promises');
|
|
1450
|
+
await writeFile3(outputPath, content, "utf-8");
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/helpers/export-formats.ts
|
|
1455
|
+
var DEFAULT_TEXT_OPTIONS = {
|
|
1456
|
+
includeTimestamps: false,
|
|
1457
|
+
includeMetadata: true
|
|
1458
|
+
};
|
|
1459
|
+
var DEFAULT_CSV_OPTIONS = {
|
|
1460
|
+
includeHeader: true,
|
|
1461
|
+
delimiter: ","
|
|
1462
|
+
};
|
|
1463
|
+
function transcriptToText(transcript, options = {}) {
|
|
1464
|
+
const opts = { ...DEFAULT_TEXT_OPTIONS, ...options };
|
|
1465
|
+
const sections = [];
|
|
1466
|
+
if (opts.includeMetadata) {
|
|
1467
|
+
sections.push(formatTextMetadata(transcript));
|
|
1468
|
+
}
|
|
1469
|
+
if (transcript.sentences && transcript.sentences.length > 0) {
|
|
1470
|
+
sections.push(formatTextTranscript(transcript.sentences, opts));
|
|
1471
|
+
}
|
|
1472
|
+
return sections.join("\n\n");
|
|
1473
|
+
}
|
|
1474
|
+
function transcriptToCsv(transcript, options = {}) {
|
|
1475
|
+
const opts = { ...DEFAULT_CSV_OPTIONS, ...options };
|
|
1476
|
+
const d = opts.delimiter;
|
|
1477
|
+
const lines = [];
|
|
1478
|
+
if (opts.includeHeader) {
|
|
1479
|
+
lines.push(`timestamp${d}speaker${d}text${d}is_question${d}is_task`);
|
|
1480
|
+
}
|
|
1481
|
+
for (const sentence of transcript.sentences) {
|
|
1482
|
+
const isQuestion = Boolean(sentence.ai_filters?.question);
|
|
1483
|
+
const isTask = Boolean(sentence.ai_filters?.task);
|
|
1484
|
+
const row = [
|
|
1485
|
+
sentence.start_time,
|
|
1486
|
+
escapeCsvField(sentence.speaker_name, d),
|
|
1487
|
+
escapeCsvField(sentence.text, d),
|
|
1488
|
+
String(isQuestion),
|
|
1489
|
+
String(isTask)
|
|
1490
|
+
];
|
|
1491
|
+
lines.push(row.join(d));
|
|
1492
|
+
}
|
|
1493
|
+
return lines.join("\n");
|
|
1494
|
+
}
|
|
1495
|
+
function sanitizeFilename(title) {
|
|
1496
|
+
if (!title.trim()) {
|
|
1497
|
+
return "untitled";
|
|
1498
|
+
}
|
|
1499
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 100);
|
|
1500
|
+
}
|
|
1501
|
+
function generateExportFilename(transcript, extension) {
|
|
1502
|
+
const sanitizedTitle = sanitizeFilename(transcript.title);
|
|
1503
|
+
let datePrefix = "";
|
|
1504
|
+
if (transcript.dateString) {
|
|
1505
|
+
try {
|
|
1506
|
+
const date = new Date(transcript.dateString);
|
|
1507
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1508
|
+
datePrefix = `${date.toISOString().slice(0, 10)}-`;
|
|
1509
|
+
}
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return `${datePrefix}${sanitizedTitle}.${extension}`;
|
|
1514
|
+
}
|
|
1515
|
+
async function exportTranscript(transcript, format) {
|
|
1516
|
+
switch (format) {
|
|
1517
|
+
case "markdown":
|
|
1518
|
+
return transcriptToMarkdown(transcript);
|
|
1519
|
+
case "json":
|
|
1520
|
+
return JSON.stringify(transcript, null, 2);
|
|
1521
|
+
case "txt":
|
|
1522
|
+
return transcriptToText(transcript);
|
|
1523
|
+
case "csv":
|
|
1524
|
+
return transcriptToCsv(transcript);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function createZipArchive(files) {
|
|
1528
|
+
const archiver = await import('archiver');
|
|
1529
|
+
const { Writable } = await import('stream');
|
|
1530
|
+
return new Promise((resolve, reject) => {
|
|
1531
|
+
const chunks = [];
|
|
1532
|
+
const archive = archiver.default("zip", { zlib: { level: 9 } });
|
|
1533
|
+
const writable = new Writable({
|
|
1534
|
+
write(chunk, _encoding, callback) {
|
|
1535
|
+
chunks.push(chunk);
|
|
1536
|
+
callback();
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
writable.on("finish", () => {
|
|
1540
|
+
resolve(Buffer.concat(chunks));
|
|
1541
|
+
});
|
|
1542
|
+
archive.on("error", reject);
|
|
1543
|
+
archive.pipe(writable);
|
|
1544
|
+
for (const file of files) {
|
|
1545
|
+
archive.append(file.content, { name: file.filename });
|
|
1546
|
+
}
|
|
1547
|
+
archive.finalize();
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function formatTextMetadata(transcript) {
|
|
1551
|
+
const lines = [];
|
|
1552
|
+
lines.push(transcript.title || "Untitled Meeting");
|
|
1553
|
+
if (transcript.dateString) {
|
|
1554
|
+
lines.push(`Date: ${formatDate2(transcript.dateString)}`);
|
|
1555
|
+
}
|
|
1556
|
+
const participants = getParticipantNames2(transcript);
|
|
1557
|
+
if (participants.length > 0) {
|
|
1558
|
+
lines.push(`Participants: ${participants.join(", ")}`);
|
|
1559
|
+
}
|
|
1560
|
+
return lines.join("\n");
|
|
1561
|
+
}
|
|
1562
|
+
function formatTextTranscript(sentences, opts) {
|
|
1563
|
+
const groups = groupSentencesBySpeaker2(sentences);
|
|
1564
|
+
const lines = [];
|
|
1565
|
+
for (const group of groups) {
|
|
1566
|
+
const text = group.sentences.map((s) => s.text).join(" ");
|
|
1567
|
+
const speaker = group.speakerName || "Unknown";
|
|
1568
|
+
const firstSentence = group.sentences[0];
|
|
1569
|
+
if (opts.includeTimestamps && firstSentence) {
|
|
1570
|
+
const timestamp = formatTimestamp2(firstSentence.start_time);
|
|
1571
|
+
lines.push(`${timestamp} ${speaker}: ${text}`);
|
|
1572
|
+
} else {
|
|
1573
|
+
lines.push(`${speaker}: ${text}`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return lines.join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
function groupSentencesBySpeaker2(sentences) {
|
|
1579
|
+
const groups = [];
|
|
1580
|
+
let current = null;
|
|
1581
|
+
for (const sentence of sentences) {
|
|
1582
|
+
if (!current || current.speakerName !== sentence.speaker_name) {
|
|
1583
|
+
current = { speakerName: sentence.speaker_name, sentences: [] };
|
|
1584
|
+
groups.push(current);
|
|
1585
|
+
}
|
|
1586
|
+
current.sentences.push(sentence);
|
|
1587
|
+
}
|
|
1588
|
+
return groups;
|
|
1589
|
+
}
|
|
1590
|
+
function formatTimestamp2(startTime) {
|
|
1591
|
+
const seconds = parseFloat(startTime);
|
|
1592
|
+
const mins = Math.floor(seconds / 60);
|
|
1593
|
+
const secs = Math.floor(seconds % 60);
|
|
1594
|
+
return `[${mins}:${secs.toString().padStart(2, "0")}]`;
|
|
1595
|
+
}
|
|
1596
|
+
function formatDate2(isoString) {
|
|
1597
|
+
return new Date(isoString).toLocaleDateString("en-US", {
|
|
1598
|
+
weekday: "long",
|
|
1599
|
+
year: "numeric",
|
|
1600
|
+
month: "long",
|
|
1601
|
+
day: "numeric"
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
function getParticipantNames2(transcript) {
|
|
1605
|
+
if (transcript.meeting_attendees?.length) {
|
|
1606
|
+
return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
|
|
1607
|
+
}
|
|
1608
|
+
return transcript.speakers?.map((s) => s.name) || [];
|
|
1609
|
+
}
|
|
1610
|
+
function escapeCsvField(field, delimiter) {
|
|
1611
|
+
if (field.includes('"') || field.includes(delimiter) || field.includes("\n")) {
|
|
1612
|
+
return `"${field.replace(/"/g, '""')}"`;
|
|
1613
|
+
}
|
|
1614
|
+
return field;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1250
1617
|
// src/helpers/meeting-insights.ts
|
|
1251
1618
|
function analyzeMeetings(transcripts, options = {}) {
|
|
1252
1619
|
const { speakers, groupBy: groupBy2, topSpeakersCount = 10, topParticipantsCount = 10 } = options;
|
|
@@ -1720,6 +2087,21 @@ var TRANSCRIPT_LIST_FIELDS = `
|
|
|
1720
2087
|
summary_status
|
|
1721
2088
|
}
|
|
1722
2089
|
`;
|
|
2090
|
+
function buildListFields(params) {
|
|
2091
|
+
const includeSentences = params?.includeSentences === true;
|
|
2092
|
+
const includeSummary = params?.includeSummary === true;
|
|
2093
|
+
if (!includeSentences && !includeSummary) {
|
|
2094
|
+
return TRANSCRIPT_LIST_FIELDS;
|
|
2095
|
+
}
|
|
2096
|
+
let fields = TRANSCRIPT_BASE_FIELDS;
|
|
2097
|
+
if (includeSentences) {
|
|
2098
|
+
fields += SENTENCES_FIELDS;
|
|
2099
|
+
}
|
|
2100
|
+
if (includeSummary) {
|
|
2101
|
+
fields += SUMMARY_FIELDS;
|
|
2102
|
+
}
|
|
2103
|
+
return fields;
|
|
2104
|
+
}
|
|
1723
2105
|
function createTranscriptsAPI(client) {
|
|
1724
2106
|
return {
|
|
1725
2107
|
async get(id, params) {
|
|
@@ -1735,6 +2117,7 @@ function createTranscriptsAPI(client) {
|
|
|
1735
2117
|
return normalizeTranscript(data.transcript);
|
|
1736
2118
|
},
|
|
1737
2119
|
async list(params) {
|
|
2120
|
+
const fields = buildListFields(params);
|
|
1738
2121
|
const query = `
|
|
1739
2122
|
query ListTranscripts(
|
|
1740
2123
|
$keyword: String
|
|
@@ -1772,13 +2155,23 @@ function createTranscriptsAPI(client) {
|
|
|
1772
2155
|
participant_email: $participant_email
|
|
1773
2156
|
date: $date
|
|
1774
2157
|
) {
|
|
1775
|
-
${
|
|
2158
|
+
${fields}
|
|
1776
2159
|
}
|
|
1777
2160
|
}
|
|
1778
2161
|
`;
|
|
2162
|
+
let internalDomain;
|
|
2163
|
+
if (params?.external) {
|
|
2164
|
+
const userQuery = "query { user { email } }";
|
|
2165
|
+
const userData = await client.execute(userQuery);
|
|
2166
|
+
internalDomain = extractDomain(userData.user.email);
|
|
2167
|
+
}
|
|
1779
2168
|
const variables = buildListVariables(params);
|
|
1780
2169
|
const data = await client.execute(query, variables);
|
|
1781
|
-
|
|
2170
|
+
let results = data.transcripts.map(normalizeTranscript);
|
|
2171
|
+
if (internalDomain) {
|
|
2172
|
+
results = results.filter((t) => hasExternalParticipants(t.participants, internalDomain));
|
|
2173
|
+
}
|
|
2174
|
+
return results;
|
|
1782
2175
|
},
|
|
1783
2176
|
async getSummary(id) {
|
|
1784
2177
|
const query = `
|
|
@@ -1825,9 +2218,10 @@ function createTranscriptsAPI(client) {
|
|
|
1825
2218
|
} = params;
|
|
1826
2219
|
const transcripts = [];
|
|
1827
2220
|
for await (const t of this.listAll({
|
|
2221
|
+
...listParams,
|
|
1828
2222
|
keyword: query,
|
|
1829
2223
|
scope,
|
|
1830
|
-
|
|
2224
|
+
includeSentences: true
|
|
1831
2225
|
})) {
|
|
1832
2226
|
transcripts.push(t);
|
|
1833
2227
|
if (limit && transcripts.length >= limit) break;
|
|
@@ -1835,8 +2229,7 @@ function createTranscriptsAPI(client) {
|
|
|
1835
2229
|
const allMatches = [];
|
|
1836
2230
|
let transcriptsWithMatches = 0;
|
|
1837
2231
|
for (const t of transcripts) {
|
|
1838
|
-
const
|
|
1839
|
-
const matches = searchTranscript(full, {
|
|
2232
|
+
const matches = searchTranscript(t, {
|
|
1840
2233
|
query,
|
|
1841
2234
|
caseSensitive,
|
|
1842
2235
|
speakers,
|
|
@@ -1888,13 +2281,13 @@ function createTranscriptsAPI(client) {
|
|
|
1888
2281
|
organizers,
|
|
1889
2282
|
participants,
|
|
1890
2283
|
user_id,
|
|
1891
|
-
channel_id
|
|
2284
|
+
channel_id,
|
|
2285
|
+
includeSentences: true
|
|
1892
2286
|
})) {
|
|
1893
2287
|
if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
|
|
1894
2288
|
continue;
|
|
1895
2289
|
}
|
|
1896
|
-
|
|
1897
|
-
transcripts.push(full);
|
|
2290
|
+
transcripts.push(t);
|
|
1898
2291
|
if (limit && transcripts.length >= limit) break;
|
|
1899
2292
|
}
|
|
1900
2293
|
return analyzeMeetings(transcripts, {
|
|
@@ -1912,13 +2305,20 @@ function createTranscriptsAPI(client) {
|
|
|
1912
2305
|
toDate,
|
|
1913
2306
|
mine,
|
|
1914
2307
|
organizers,
|
|
1915
|
-
participants
|
|
2308
|
+
participants,
|
|
2309
|
+
includeSummary: true
|
|
1916
2310
|
})) {
|
|
1917
|
-
|
|
1918
|
-
transcripts.push(full);
|
|
2311
|
+
transcripts.push(t);
|
|
1919
2312
|
if (limit && transcripts.length >= limit) break;
|
|
1920
2313
|
}
|
|
1921
2314
|
return aggregateActionItems(transcripts, {}, filterOptions);
|
|
2315
|
+
},
|
|
2316
|
+
async bulkExport(params = {}) {
|
|
2317
|
+
const { format = "markdown", asZip = false, onProgress } = params;
|
|
2318
|
+
const transcriptIds = await collectTranscriptIds(client, this, params);
|
|
2319
|
+
const files = await convertTranscripts(this, transcriptIds, format, onProgress);
|
|
2320
|
+
const zip = asZip ? await createZipArchive(files) : void 0;
|
|
2321
|
+
return { files, zip, format, totalExported: files.length };
|
|
1922
2322
|
}
|
|
1923
2323
|
};
|
|
1924
2324
|
}
|
|
@@ -1997,6 +2397,50 @@ function buildListVariables(params) {
|
|
|
1997
2397
|
date: params.date
|
|
1998
2398
|
};
|
|
1999
2399
|
}
|
|
2400
|
+
async function collectTranscriptIds(client, api, params) {
|
|
2401
|
+
const { ids, fromDate, toDate, mine, organizers, participants, external, limit } = params;
|
|
2402
|
+
if (ids?.length) {
|
|
2403
|
+
return ids;
|
|
2404
|
+
}
|
|
2405
|
+
let internalDomain;
|
|
2406
|
+
if (external) {
|
|
2407
|
+
const userQuery = "query { user { email } }";
|
|
2408
|
+
const userData = await client.execute(userQuery);
|
|
2409
|
+
internalDomain = extractDomain(userData.user.email);
|
|
2410
|
+
}
|
|
2411
|
+
const result = [];
|
|
2412
|
+
for await (const t of api.listAll({ fromDate, toDate, mine, organizers, participants })) {
|
|
2413
|
+
if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
result.push(t.id);
|
|
2417
|
+
if (limit && result.length >= limit) break;
|
|
2418
|
+
}
|
|
2419
|
+
return result;
|
|
2420
|
+
}
|
|
2421
|
+
async function convertTranscripts(api, transcriptIds, format, onProgress) {
|
|
2422
|
+
const files = [];
|
|
2423
|
+
const extension = format === "markdown" ? "md" : format;
|
|
2424
|
+
let completed = 0;
|
|
2425
|
+
const total = transcriptIds.length;
|
|
2426
|
+
for await (const result of batch(
|
|
2427
|
+
transcriptIds,
|
|
2428
|
+
async (id) => {
|
|
2429
|
+
const transcript = await api.get(id);
|
|
2430
|
+
const content = await exportTranscript(transcript, format);
|
|
2431
|
+
const filename = generateExportFilename(transcript, extension);
|
|
2432
|
+
return { id, title: transcript.title, filename, content };
|
|
2433
|
+
},
|
|
2434
|
+
{ delayMs: 100 }
|
|
2435
|
+
)) {
|
|
2436
|
+
if (!result.error) {
|
|
2437
|
+
files.push(result.result);
|
|
2438
|
+
}
|
|
2439
|
+
completed++;
|
|
2440
|
+
onProgress?.(completed, total);
|
|
2441
|
+
}
|
|
2442
|
+
return files;
|
|
2443
|
+
}
|
|
2000
2444
|
|
|
2001
2445
|
// src/graphql/queries/users.ts
|
|
2002
2446
|
var USER_FIELDS = `
|
|
@@ -2458,6 +2902,10 @@ function getOutputFormat(cmd) {
|
|
|
2458
2902
|
const opts = cmd.optsWithGlobals();
|
|
2459
2903
|
return opts.output ?? "json";
|
|
2460
2904
|
}
|
|
2905
|
+
function isProgressEnabled(cmd) {
|
|
2906
|
+
const opts = cmd.optsWithGlobals();
|
|
2907
|
+
return opts.progress ?? false;
|
|
2908
|
+
}
|
|
2461
2909
|
|
|
2462
2910
|
// src/cli/utils/error.ts
|
|
2463
2911
|
function handleError(error) {
|
|
@@ -2692,7 +3140,7 @@ function registerAiAppsCommand(program2) {
|
|
|
2692
3140
|
}
|
|
2693
3141
|
|
|
2694
3142
|
// src/cli/utils/parse.ts
|
|
2695
|
-
function
|
|
3143
|
+
function formatDuration2(seconds) {
|
|
2696
3144
|
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
2697
3145
|
return "0s";
|
|
2698
3146
|
}
|
|
@@ -2845,170 +3293,815 @@ function registerBitesCommand(program2) {
|
|
|
2845
3293
|
);
|
|
2846
3294
|
}
|
|
2847
3295
|
|
|
2848
|
-
// src/helpers/
|
|
2849
|
-
var
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
actionItemFormat: "checkbox",
|
|
2854
|
-
includeTimestamps: false,
|
|
2855
|
-
speakerFormat: "bold",
|
|
2856
|
-
groupBySpeaker: true
|
|
2857
|
-
};
|
|
2858
|
-
async function transcriptToMarkdown(transcript, options = {}) {
|
|
2859
|
-
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
2860
|
-
const sections = [];
|
|
2861
|
-
if (opts.includeMetadata) {
|
|
2862
|
-
sections.push(formatMetadata(transcript));
|
|
3296
|
+
// src/helpers/digest.ts
|
|
3297
|
+
var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
3298
|
+
function calculateStats(transcripts) {
|
|
3299
|
+
if (transcripts.length === 0) {
|
|
3300
|
+
return emptyStats();
|
|
2863
3301
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
3302
|
+
const totalMinutes = sumDurations2(transcripts);
|
|
3303
|
+
const meetingsByDay = calculateMeetingsByDay(transcripts);
|
|
3304
|
+
const busiestDay = findBusiestDay(meetingsByDay);
|
|
3305
|
+
return {
|
|
3306
|
+
totalMeetings: transcripts.length,
|
|
3307
|
+
totalMinutes,
|
|
3308
|
+
averageDuration: totalMinutes / transcripts.length,
|
|
3309
|
+
busiestDay,
|
|
3310
|
+
meetingsByDay
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
function emptyStats() {
|
|
3314
|
+
return {
|
|
3315
|
+
totalMeetings: 0,
|
|
3316
|
+
totalMinutes: 0,
|
|
3317
|
+
averageDuration: 0,
|
|
3318
|
+
busiestDay: "",
|
|
3319
|
+
meetingsByDay: {}
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
function sumDurations2(transcripts) {
|
|
3323
|
+
return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
|
|
3324
|
+
}
|
|
3325
|
+
function calculateMeetingsByDay(transcripts) {
|
|
3326
|
+
const counts = {};
|
|
3327
|
+
for (const t of transcripts) {
|
|
3328
|
+
const date = parseDate2(t.dateString);
|
|
3329
|
+
if (!date) continue;
|
|
3330
|
+
const dayName = DAY_NAMES[date.getUTCDay()];
|
|
3331
|
+
if (dayName) {
|
|
3332
|
+
counts[dayName] = (counts[dayName] ?? 0) + 1;
|
|
3333
|
+
}
|
|
2866
3334
|
}
|
|
2867
|
-
|
|
2868
|
-
|
|
3335
|
+
return counts;
|
|
3336
|
+
}
|
|
3337
|
+
function findBusiestDay(meetingsByDay) {
|
|
3338
|
+
let busiestDay = "";
|
|
3339
|
+
let maxCount = 0;
|
|
3340
|
+
for (const [day, count] of Object.entries(meetingsByDay)) {
|
|
3341
|
+
if (count > maxCount) {
|
|
3342
|
+
maxCount = count;
|
|
3343
|
+
busiestDay = day;
|
|
3344
|
+
}
|
|
2869
3345
|
}
|
|
2870
|
-
|
|
2871
|
-
await writeIfOutputPath(content, options.outputPath);
|
|
2872
|
-
return content;
|
|
3346
|
+
return busiestDay;
|
|
2873
3347
|
}
|
|
2874
|
-
function
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3348
|
+
function parseDate2(dateString) {
|
|
3349
|
+
if (!dateString) return null;
|
|
3350
|
+
const date = new Date(dateString);
|
|
3351
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
3352
|
+
}
|
|
3353
|
+
var MAX_KEY_POINTS_PER_MEETING = 5;
|
|
3354
|
+
function extractHighlights(transcripts) {
|
|
3355
|
+
const highlights = [];
|
|
3356
|
+
for (const t of transcripts) {
|
|
3357
|
+
const overview = t.summary?.overview;
|
|
3358
|
+
if (!overview || overview.trim().length === 0) continue;
|
|
3359
|
+
const keyPoints = extractKeyPoints(overview);
|
|
3360
|
+
if (keyPoints.length === 0) continue;
|
|
3361
|
+
highlights.push({
|
|
3362
|
+
meetingId: t.id,
|
|
3363
|
+
meetingTitle: t.title,
|
|
3364
|
+
meetingDate: t.dateString,
|
|
3365
|
+
keyPoints,
|
|
3366
|
+
decisions: extractDecisions(t)
|
|
3367
|
+
});
|
|
2879
3368
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3369
|
+
return highlights;
|
|
3370
|
+
}
|
|
3371
|
+
function extractKeyPoints(overview) {
|
|
3372
|
+
const sentences = overview.split(/(?<=[.!?])\s+/).map((s) => s.trim().replace(/[.!?]$/, "")).map((s) => s.replace(/^-\s*/, "")).filter((s) => s.length > 0);
|
|
3373
|
+
return sentences.slice(0, MAX_KEY_POINTS_PER_MEETING);
|
|
3374
|
+
}
|
|
3375
|
+
function extractDecisions(transcript) {
|
|
3376
|
+
const decisions = [];
|
|
3377
|
+
const overview = transcript.summary?.overview ?? "";
|
|
3378
|
+
const decisionPattern = /(?:^|[.!?]\s*)([^.!?]*(?:decided?|decision|agreed|approved)[^.!?]*)/gi;
|
|
3379
|
+
for (; ; ) {
|
|
3380
|
+
const match = decisionPattern.exec(overview);
|
|
3381
|
+
if (!match) break;
|
|
3382
|
+
if (match[1]) {
|
|
3383
|
+
decisions.push(match[1].trim());
|
|
3384
|
+
}
|
|
2883
3385
|
}
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
3386
|
+
return decisions;
|
|
3387
|
+
}
|
|
3388
|
+
function buildNameLookup(transcripts) {
|
|
3389
|
+
const namesByEmail = /* @__PURE__ */ new Map();
|
|
3390
|
+
for (const t of transcripts) {
|
|
3391
|
+
for (const attendee of t.meeting_attendees ?? []) {
|
|
3392
|
+
if (!attendee.email) continue;
|
|
3393
|
+
const normalizedEmail = attendee.email.toLowerCase();
|
|
3394
|
+
const name = attendee.displayName || attendee.name;
|
|
3395
|
+
if (name && !namesByEmail.has(normalizedEmail)) {
|
|
3396
|
+
namesByEmail.set(normalizedEmail, name);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
2887
3399
|
}
|
|
2888
|
-
return
|
|
3400
|
+
return namesByEmail;
|
|
2889
3401
|
}
|
|
2890
|
-
function
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3402
|
+
function countParticipation(transcripts, namesByEmail) {
|
|
3403
|
+
const stats = /* @__PURE__ */ new Map();
|
|
3404
|
+
for (const t of transcripts) {
|
|
3405
|
+
const seenInMeeting = /* @__PURE__ */ new Set();
|
|
3406
|
+
for (const email of t.participants ?? []) {
|
|
3407
|
+
const normalizedEmail = email.toLowerCase();
|
|
3408
|
+
if (seenInMeeting.has(normalizedEmail)) continue;
|
|
3409
|
+
seenInMeeting.add(normalizedEmail);
|
|
3410
|
+
const existing = stats.get(normalizedEmail) ?? {
|
|
3411
|
+
name: namesByEmail.get(normalizedEmail) || extractNameFromEmail(email),
|
|
3412
|
+
meetingCount: 0,
|
|
3413
|
+
totalMinutes: 0
|
|
3414
|
+
};
|
|
3415
|
+
existing.meetingCount++;
|
|
3416
|
+
existing.totalMinutes += t.duration ?? 0;
|
|
3417
|
+
stats.set(normalizedEmail, existing);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
return stats;
|
|
3421
|
+
}
|
|
3422
|
+
function aggregateParticipants2(transcripts) {
|
|
3423
|
+
const namesByEmail = buildNameLookup(transcripts);
|
|
3424
|
+
const stats = countParticipation(transcripts, namesByEmail);
|
|
3425
|
+
return Array.from(stats, ([email, data]) => ({
|
|
3426
|
+
email,
|
|
3427
|
+
name: data.name,
|
|
3428
|
+
meetingCount: data.meetingCount,
|
|
3429
|
+
totalMinutes: data.totalMinutes
|
|
3430
|
+
})).sort((a, b) => b.meetingCount - a.meetingCount);
|
|
3431
|
+
}
|
|
3432
|
+
function extractNameFromEmail(email) {
|
|
3433
|
+
const localPart = email.split("@")[0] ?? email;
|
|
3434
|
+
return localPart;
|
|
3435
|
+
}
|
|
3436
|
+
function buildParticipantInfoList(emails, attendees) {
|
|
3437
|
+
const attendeeMap = /* @__PURE__ */ new Map();
|
|
3438
|
+
for (const attendee of attendees) {
|
|
3439
|
+
if (attendee.email) {
|
|
3440
|
+
attendeeMap.set(attendee.email.toLowerCase(), attendee);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return emails.map((email) => {
|
|
3444
|
+
const normalizedEmail = email.toLowerCase();
|
|
3445
|
+
const attendee = attendeeMap.get(normalizedEmail);
|
|
3446
|
+
return {
|
|
3447
|
+
email: normalizedEmail,
|
|
3448
|
+
name: attendee?.displayName || attendee?.name || extractNameFromEmail(normalizedEmail)
|
|
3449
|
+
};
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
function aggregateActionItemsForDigest(transcripts) {
|
|
3453
|
+
if (transcripts.length === 0) {
|
|
3454
|
+
return emptyActionItems();
|
|
3455
|
+
}
|
|
3456
|
+
const byAssignee = {};
|
|
3457
|
+
const byMeeting = [];
|
|
3458
|
+
const unassigned = [];
|
|
3459
|
+
const withDueDates = [];
|
|
3460
|
+
let total = 0;
|
|
3461
|
+
for (const t of transcripts) {
|
|
3462
|
+
const result = extractActionItems(t);
|
|
3463
|
+
const meetingItems = [];
|
|
3464
|
+
for (const item of result.items) {
|
|
3465
|
+
const digestItem = {
|
|
3466
|
+
...item,
|
|
3467
|
+
transcriptId: t.id,
|
|
3468
|
+
transcriptTitle: t.title,
|
|
3469
|
+
transcriptDate: t.dateString
|
|
3470
|
+
};
|
|
3471
|
+
total++;
|
|
3472
|
+
meetingItems.push(digestItem);
|
|
3473
|
+
if (item.assignee) {
|
|
3474
|
+
const existing = byAssignee[item.assignee] ?? [];
|
|
3475
|
+
existing.push(digestItem);
|
|
3476
|
+
byAssignee[item.assignee] = existing;
|
|
3477
|
+
} else {
|
|
3478
|
+
unassigned.push(digestItem);
|
|
3479
|
+
}
|
|
3480
|
+
if (item.dueDate) {
|
|
3481
|
+
withDueDates.push(digestItem);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
if (meetingItems.length > 0) {
|
|
3485
|
+
byMeeting.push({
|
|
3486
|
+
id: t.id,
|
|
3487
|
+
title: t.title,
|
|
3488
|
+
date: t.dateString,
|
|
3489
|
+
duration: t.duration ?? 0,
|
|
3490
|
+
participants: buildParticipantInfoList(t.participants ?? [], t.meeting_attendees ?? []),
|
|
3491
|
+
items: meetingItems
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
return { total, byAssignee, byMeeting, unassigned, withDueDates };
|
|
3496
|
+
}
|
|
3497
|
+
function emptyActionItems() {
|
|
3498
|
+
return {
|
|
3499
|
+
total: 0,
|
|
3500
|
+
byAssignee: {},
|
|
3501
|
+
byMeeting: [],
|
|
3502
|
+
unassigned: [],
|
|
3503
|
+
withDueDates: []
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
function buildDigest(transcripts, options = {}) {
|
|
3507
|
+
const { includeActionItems = true, includeHighlights = true, includeStats = true } = options;
|
|
3508
|
+
if (transcripts.length === 0) {
|
|
3509
|
+
return emptyDigest();
|
|
3510
|
+
}
|
|
3511
|
+
const stats = includeStats ? calculateStats(transcripts) : emptyStats();
|
|
3512
|
+
const actionItems = includeActionItems ? aggregateActionItemsForDigest(transcripts) : emptyActionItems();
|
|
3513
|
+
const highlights = includeHighlights ? extractHighlights(transcripts) : [];
|
|
3514
|
+
const participants = aggregateParticipants2(transcripts);
|
|
3515
|
+
const meetings = transcripts.map(toMeetingSummary);
|
|
3516
|
+
const period = calculatePeriod(transcripts);
|
|
3517
|
+
const totalDuration = sumDurations2(transcripts);
|
|
3518
|
+
return {
|
|
3519
|
+
period,
|
|
3520
|
+
totalMeetings: transcripts.length,
|
|
3521
|
+
totalDuration,
|
|
3522
|
+
stats,
|
|
3523
|
+
actionItems,
|
|
3524
|
+
highlights,
|
|
3525
|
+
participants,
|
|
3526
|
+
meetings
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
function emptyDigest() {
|
|
3530
|
+
return {
|
|
3531
|
+
period: { from: "", to: "" },
|
|
3532
|
+
totalMeetings: 0,
|
|
3533
|
+
totalDuration: 0,
|
|
3534
|
+
stats: emptyStats(),
|
|
3535
|
+
actionItems: emptyActionItems(),
|
|
3536
|
+
highlights: [],
|
|
3537
|
+
participants: [],
|
|
3538
|
+
meetings: []
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
function toMeetingSummary(transcript) {
|
|
3542
|
+
return {
|
|
3543
|
+
id: transcript.id,
|
|
3544
|
+
title: transcript.title,
|
|
3545
|
+
date: transcript.dateString,
|
|
3546
|
+
duration: transcript.duration ?? 0,
|
|
3547
|
+
participants: (transcript.participants ?? []).length
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
function calculatePeriod(transcripts) {
|
|
3551
|
+
let earliest = null;
|
|
3552
|
+
let latest = null;
|
|
3553
|
+
for (const t of transcripts) {
|
|
3554
|
+
const date = parseDate2(t.dateString);
|
|
3555
|
+
if (!date) continue;
|
|
3556
|
+
if (!earliest || date < earliest) {
|
|
3557
|
+
earliest = date;
|
|
3558
|
+
}
|
|
3559
|
+
if (!latest || date > latest) {
|
|
3560
|
+
latest = date;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return {
|
|
3564
|
+
from: earliest ? formatDateOnly2(earliest) : "",
|
|
3565
|
+
to: latest ? formatDateOnly2(latest) : ""
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
function formatDateOnly2(date) {
|
|
3569
|
+
const year = date.getUTCFullYear();
|
|
3570
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
3571
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
3572
|
+
return `${year}-${month}-${day}`;
|
|
3573
|
+
}
|
|
3574
|
+
var BUILT_IN_TEMPLATES = ["default", "compact", "executive"];
|
|
3575
|
+
function getTemplatesDir() {
|
|
3576
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
3577
|
+
return join(currentDir, "..", "templates", "digest");
|
|
3578
|
+
}
|
|
3579
|
+
function isBuiltInTemplate(name) {
|
|
3580
|
+
return BUILT_IN_TEMPLATES.includes(name);
|
|
3581
|
+
}
|
|
3582
|
+
function loadTemplate(templateOption) {
|
|
3583
|
+
const templateName = templateOption ?? "default";
|
|
3584
|
+
if (isBuiltInTemplate(templateName)) {
|
|
3585
|
+
const templatePath = join(getTemplatesDir(), `${templateName}.md`);
|
|
3586
|
+
try {
|
|
3587
|
+
return readFileSync(templatePath, "utf-8");
|
|
3588
|
+
} catch {
|
|
3589
|
+
return getInlineTemplate(templateName);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
if (templateOption && (templateOption.startsWith("#") || templateOption.includes("{{"))) {
|
|
3593
|
+
return templateOption;
|
|
3594
|
+
}
|
|
3595
|
+
try {
|
|
3596
|
+
return readFileSync(templateName, "utf-8");
|
|
3597
|
+
} catch {
|
|
3598
|
+
if (templateName.includes("/") || templateName.includes("\\") || templateName.endsWith(".md")) {
|
|
3599
|
+
throw new Error(`Template file not found: ${templateName}`);
|
|
3600
|
+
}
|
|
3601
|
+
return templateName;
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
function getInlineTemplate(name) {
|
|
3605
|
+
switch (name) {
|
|
3606
|
+
case "compact":
|
|
3607
|
+
return COMPACT_TEMPLATE;
|
|
3608
|
+
case "executive":
|
|
3609
|
+
return EXECUTIVE_TEMPLATE;
|
|
3610
|
+
default:
|
|
3611
|
+
return DEFAULT_TEMPLATE;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
function renderDigest(digest, options) {
|
|
3615
|
+
const template = loadTemplate(options?.template);
|
|
3616
|
+
return renderTemplate(template, digest);
|
|
3617
|
+
}
|
|
3618
|
+
function formatDurationHtml(minutes) {
|
|
3619
|
+
const hours = Math.floor(minutes / 60);
|
|
3620
|
+
const mins = minutes % 60;
|
|
3621
|
+
return `${hours}h ${mins}m`;
|
|
3622
|
+
}
|
|
3623
|
+
function escapeHtml(str) {
|
|
3624
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3625
|
+
}
|
|
3626
|
+
function renderActionItemsHtml(digest) {
|
|
3627
|
+
const allActionItems = digest.actionItems.byMeeting.flatMap((m) => m.items);
|
|
3628
|
+
return allActionItems.map(
|
|
3629
|
+
(item) => `
|
|
3630
|
+
<li class="action-item">
|
|
3631
|
+
<input type="checkbox" disabled />
|
|
3632
|
+
<span class="action-text">${escapeHtml(item.text)}</span>
|
|
3633
|
+
${item.assignee ? `<span class="assignee">(${escapeHtml(item.assignee)})</span>` : ""}
|
|
3634
|
+
${item.dueDate ? `<span class="due-date">due ${escapeHtml(item.dueDate)}</span>` : ""}
|
|
3635
|
+
</li>`
|
|
3636
|
+
).join("\n");
|
|
3637
|
+
}
|
|
3638
|
+
function renderMeetingsHtml(digest) {
|
|
3639
|
+
return digest.meetings.map(
|
|
3640
|
+
(m) => `
|
|
3641
|
+
<tr>
|
|
3642
|
+
<td>${escapeHtml(m.title)}</td>
|
|
3643
|
+
<td>${escapeHtml(m.date)}</td>
|
|
3644
|
+
<td>${formatDurationHtml(m.duration)}</td>
|
|
3645
|
+
<td>${m.participants}</td>
|
|
3646
|
+
</tr>`
|
|
3647
|
+
).join("\n");
|
|
3648
|
+
}
|
|
3649
|
+
function renderHighlightsHtml(digest) {
|
|
3650
|
+
return digest.highlights.map(
|
|
3651
|
+
(h) => `
|
|
3652
|
+
<div class="highlight">
|
|
3653
|
+
<h4>${escapeHtml(h.meetingTitle)} (${escapeHtml(h.meetingDate)})</h4>
|
|
3654
|
+
<ul>
|
|
3655
|
+
${h.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join("\n")}
|
|
3656
|
+
</ul>
|
|
3657
|
+
</div>`
|
|
3658
|
+
).join("\n");
|
|
3659
|
+
}
|
|
3660
|
+
function renderParticipantsHtml(digest) {
|
|
3661
|
+
return digest.participants.map(
|
|
3662
|
+
(p) => `
|
|
3663
|
+
<tr>
|
|
3664
|
+
<td>${escapeHtml(p.name || p.email)}</td>
|
|
3665
|
+
<td>${escapeHtml(p.email)}</td>
|
|
3666
|
+
<td>${p.meetingCount}</td>
|
|
3667
|
+
<td>${formatDurationHtml(p.totalMinutes)}</td>
|
|
3668
|
+
</tr>`
|
|
3669
|
+
).join("\n");
|
|
3670
|
+
}
|
|
3671
|
+
function renderDigestHtml(digest) {
|
|
3672
|
+
const actionItemsHtml = renderActionItemsHtml(digest);
|
|
3673
|
+
const meetingsHtml = renderMeetingsHtml(digest);
|
|
3674
|
+
const highlightsHtml = renderHighlightsHtml(digest);
|
|
3675
|
+
const participantsHtml = renderParticipantsHtml(digest);
|
|
3676
|
+
return `<!DOCTYPE html>
|
|
3677
|
+
<html lang="en">
|
|
3678
|
+
<head>
|
|
3679
|
+
<meta charset="UTF-8">
|
|
3680
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3681
|
+
<title>Weekly Meeting Digest</title>
|
|
3682
|
+
<style>
|
|
3683
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; line-height: 1.6; }
|
|
3684
|
+
h1 { color: #333; border-bottom: 2px solid #4a90d9; padding-bottom: 10px; }
|
|
3685
|
+
h2 { color: #4a90d9; margin-top: 30px; }
|
|
3686
|
+
h3 { color: #666; }
|
|
3687
|
+
.overview { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
|
|
3688
|
+
.stat-card { background: #f5f5f5; padding: 20px; border-radius: 8px; text-align: center; }
|
|
3689
|
+
.stat-value { font-size: 2em; font-weight: bold; color: #4a90d9; }
|
|
3690
|
+
.stat-label { color: #666; }
|
|
3691
|
+
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
|
|
3692
|
+
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
|
|
3693
|
+
th { background: #f5f5f5; font-weight: 600; }
|
|
3694
|
+
.action-item { list-style: none; padding: 8px 0; border-bottom: 1px solid #eee; }
|
|
3695
|
+
.action-item input { margin-right: 10px; }
|
|
3696
|
+
.assignee { color: #4a90d9; margin-left: 10px; }
|
|
3697
|
+
.due-date { color: #e74c3c; margin-left: 10px; font-size: 0.9em; }
|
|
3698
|
+
.highlight { background: #f9f9f9; padding: 15px; border-radius: 8px; margin: 10px 0; }
|
|
3699
|
+
.highlight h4 { margin: 0 0 10px 0; color: #333; }
|
|
3700
|
+
.highlight ul { margin: 0; padding-left: 20px; }
|
|
3701
|
+
footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #999; font-size: 0.9em; }
|
|
3702
|
+
</style>
|
|
3703
|
+
</head>
|
|
3704
|
+
<body>
|
|
3705
|
+
<h1>Weekly Meeting Digest</h1>
|
|
3706
|
+
<p><strong>${escapeHtml(digest.period.from)}</strong> to <strong>${escapeHtml(digest.period.to)}</strong></p>
|
|
3707
|
+
|
|
3708
|
+
<div class="overview">
|
|
3709
|
+
<div class="stat-card">
|
|
3710
|
+
<div class="stat-value">${digest.totalMeetings}</div>
|
|
3711
|
+
<div class="stat-label">Meetings</div>
|
|
3712
|
+
</div>
|
|
3713
|
+
<div class="stat-card">
|
|
3714
|
+
<div class="stat-value">${formatDurationHtml(digest.totalDuration)}</div>
|
|
3715
|
+
<div class="stat-label">Total Time</div>
|
|
3716
|
+
</div>
|
|
3717
|
+
<div class="stat-card">
|
|
3718
|
+
<div class="stat-value">${digest.actionItems.total}</div>
|
|
3719
|
+
<div class="stat-label">Action Items</div>
|
|
3720
|
+
</div>
|
|
3721
|
+
</div>
|
|
3722
|
+
|
|
3723
|
+
<h2>Meeting Stats</h2>
|
|
3724
|
+
<p>Busiest day: <strong>${escapeHtml(digest.stats.busiestDay)}</strong></p>
|
|
3725
|
+
<p>Average duration: <strong>${formatDurationHtml(digest.stats.averageDuration)}</strong></p>
|
|
3726
|
+
|
|
3727
|
+
<h2>Meetings</h2>
|
|
3728
|
+
<table>
|
|
3729
|
+
<thead>
|
|
3730
|
+
<tr><th>Title</th><th>Date</th><th>Duration</th><th>Participants</th></tr>
|
|
3731
|
+
</thead>
|
|
3732
|
+
<tbody>
|
|
3733
|
+
${meetingsHtml}
|
|
3734
|
+
</tbody>
|
|
3735
|
+
</table>
|
|
3736
|
+
|
|
3737
|
+
${digest.actionItems.total > 0 ? `
|
|
3738
|
+
<h2>Action Items (${digest.actionItems.total})</h2>
|
|
3739
|
+
<ul style="padding-left: 0;">
|
|
3740
|
+
${actionItemsHtml}
|
|
3741
|
+
</ul>` : ""}
|
|
3742
|
+
|
|
3743
|
+
${digest.highlights.length > 0 ? `
|
|
3744
|
+
<h2>Highlights</h2>
|
|
3745
|
+
${highlightsHtml}` : ""}
|
|
3746
|
+
|
|
3747
|
+
<h2>Participants</h2>
|
|
3748
|
+
<table>
|
|
3749
|
+
<thead>
|
|
3750
|
+
<tr><th>Name</th><th>Email</th><th>Meetings</th><th>Time</th></tr>
|
|
3751
|
+
</thead>
|
|
3752
|
+
<tbody>
|
|
3753
|
+
${participantsHtml}
|
|
3754
|
+
</tbody>
|
|
3755
|
+
</table>
|
|
3756
|
+
|
|
3757
|
+
<footer>
|
|
3758
|
+
Generated with fireflies-api
|
|
3759
|
+
</footer>
|
|
3760
|
+
</body>
|
|
3761
|
+
</html>`;
|
|
3762
|
+
}
|
|
3763
|
+
var FILTERS = {
|
|
3764
|
+
duration: (value) => {
|
|
3765
|
+
const minutes = Math.round(Number(value) || 0);
|
|
3766
|
+
const hours = Math.floor(minutes / 60);
|
|
3767
|
+
const mins = minutes % 60;
|
|
3768
|
+
return `${hours}h ${mins}m`;
|
|
3769
|
+
},
|
|
3770
|
+
date: (value) => {
|
|
3771
|
+
const str = String(value || "");
|
|
3772
|
+
if (!str) return "";
|
|
3773
|
+
const date = new Date(str);
|
|
3774
|
+
if (Number.isNaN(date.getTime())) return str;
|
|
3775
|
+
return date.toLocaleDateString("en-US", {
|
|
3776
|
+
month: "short",
|
|
3777
|
+
day: "numeric",
|
|
3778
|
+
year: "numeric"
|
|
3779
|
+
});
|
|
3780
|
+
},
|
|
3781
|
+
join: (value) => {
|
|
3782
|
+
if (Array.isArray(value)) {
|
|
3783
|
+
return value.join(", ");
|
|
3784
|
+
}
|
|
3785
|
+
return String(value || "");
|
|
3786
|
+
},
|
|
3787
|
+
lowercase: (value) => String(value || "").toLowerCase(),
|
|
3788
|
+
uppercase: (value) => String(value || "").toUpperCase()
|
|
3789
|
+
};
|
|
3790
|
+
function renderTemplate(template, data) {
|
|
3791
|
+
if (!template) return "";
|
|
3792
|
+
let result = template;
|
|
3793
|
+
result = processSections(result, data);
|
|
3794
|
+
result = processVariables(result, data);
|
|
3795
|
+
return result;
|
|
3796
|
+
}
|
|
3797
|
+
function processSections(template, data) {
|
|
3798
|
+
const sectionPattern = /\{\{#(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
|
|
3799
|
+
let result = template;
|
|
3800
|
+
let iterations = 0;
|
|
3801
|
+
const maxIterations = 100;
|
|
3802
|
+
for (; ; ) {
|
|
3803
|
+
const match = sectionPattern.exec(result);
|
|
3804
|
+
if (!match || iterations >= maxIterations) break;
|
|
3805
|
+
iterations++;
|
|
3806
|
+
const [fullMatch, path, content] = match;
|
|
3807
|
+
if (!path || content === void 0) continue;
|
|
3808
|
+
const value = getNestedValue(data, path);
|
|
3809
|
+
const replacement = renderSectionValue(value, content, data);
|
|
3810
|
+
result = result.slice(0, match.index) + replacement + result.slice(match.index + fullMatch.length);
|
|
3811
|
+
sectionPattern.lastIndex = 0;
|
|
3812
|
+
}
|
|
3813
|
+
return result;
|
|
3814
|
+
}
|
|
3815
|
+
function renderSectionValue(value, content, data) {
|
|
3816
|
+
if (Array.isArray(value)) {
|
|
3817
|
+
return renderArraySection(value, content, data);
|
|
3818
|
+
}
|
|
3819
|
+
if (isIterableObject(value)) {
|
|
3820
|
+
return renderObjectSection(value, content, data);
|
|
3821
|
+
}
|
|
3822
|
+
if (isTruthy(value)) {
|
|
3823
|
+
const processed = processSections(content, data);
|
|
3824
|
+
return processVariables(processed, data);
|
|
3825
|
+
}
|
|
3826
|
+
return "";
|
|
3827
|
+
}
|
|
3828
|
+
function renderArraySection(items, content, data) {
|
|
3829
|
+
let result = "";
|
|
3830
|
+
for (const item of items) {
|
|
3831
|
+
if (typeof item === "object" && item !== null) {
|
|
3832
|
+
const itemData = { ...data, ...item };
|
|
3833
|
+
const processed = processSections(content, itemData);
|
|
3834
|
+
result += processVariables(processed, itemData);
|
|
3835
|
+
} else {
|
|
3836
|
+
const itemContent = content.replace(/\{\{\.\}\}/g, String(item));
|
|
3837
|
+
result += processVariables(itemContent, data);
|
|
2895
3838
|
}
|
|
2896
3839
|
}
|
|
2897
|
-
return
|
|
3840
|
+
return result;
|
|
2898
3841
|
}
|
|
2899
|
-
function
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3842
|
+
function renderObjectSection(obj, content, data) {
|
|
3843
|
+
let result = "";
|
|
3844
|
+
for (const [key, itemValue] of Object.entries(obj)) {
|
|
3845
|
+
const itemData = buildObjectItemData(data, key, itemValue);
|
|
3846
|
+
const itemContent = content.replace(/\{\{\.\}\}/g, key);
|
|
3847
|
+
const processed = processSections(itemContent, itemData);
|
|
3848
|
+
result += processVariables(processed, itemData);
|
|
2903
3849
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
}
|
|
3850
|
+
return result;
|
|
3851
|
+
}
|
|
3852
|
+
function buildObjectItemData(data, key, itemValue) {
|
|
3853
|
+
const baseData = { ...data, _key: key, _value: itemValue };
|
|
3854
|
+
if (typeof itemValue === "object" && itemValue !== null && !Array.isArray(itemValue)) {
|
|
3855
|
+
return { ...baseData, ...itemValue };
|
|
2910
3856
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
3857
|
+
return baseData;
|
|
3858
|
+
}
|
|
3859
|
+
function isIterableObject(value) {
|
|
3860
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
3861
|
+
}
|
|
3862
|
+
function isTruthy(value) {
|
|
3863
|
+
if (value === null || value === void 0) return false;
|
|
3864
|
+
if (typeof value === "boolean") return value;
|
|
3865
|
+
if (typeof value === "string") return value.length > 0;
|
|
3866
|
+
if (typeof value === "number") return true;
|
|
3867
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
3868
|
+
return true;
|
|
3869
|
+
}
|
|
3870
|
+
function processVariables(template, data) {
|
|
3871
|
+
return template.replace(
|
|
3872
|
+
/\{\{([\w.]+)(?:\s*\|\s*(\w+))?\}\}/g,
|
|
3873
|
+
(_match, path, filterName) => {
|
|
3874
|
+
const value = getNestedValue(data, path);
|
|
3875
|
+
if (value === void 0 || value === null) {
|
|
3876
|
+
return "";
|
|
3877
|
+
}
|
|
3878
|
+
if (filterName && FILTERS[filterName]) {
|
|
3879
|
+
return FILTERS[filterName](value);
|
|
3880
|
+
}
|
|
3881
|
+
return String(value);
|
|
2917
3882
|
}
|
|
2918
|
-
|
|
2919
|
-
return sections.join("\n");
|
|
3883
|
+
);
|
|
2920
3884
|
}
|
|
2921
|
-
function
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3885
|
+
function getNestedValue(obj, path) {
|
|
3886
|
+
const parts = path.split(".");
|
|
3887
|
+
let current = obj;
|
|
3888
|
+
for (const part of parts) {
|
|
3889
|
+
if (current === null || current === void 0) {
|
|
3890
|
+
return void 0;
|
|
2927
3891
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
lines.push("", formatSentence(sentence, opts));
|
|
3892
|
+
if (typeof current !== "object") {
|
|
3893
|
+
return void 0;
|
|
2931
3894
|
}
|
|
3895
|
+
current = current[part];
|
|
2932
3896
|
}
|
|
2933
|
-
return
|
|
3897
|
+
return current;
|
|
2934
3898
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3899
|
+
var DEFAULT_TEMPLATE = `# Weekly Meeting Digest
|
|
3900
|
+
**{{period.from}} to {{period.to}}**
|
|
3901
|
+
|
|
3902
|
+
## Overview
|
|
3903
|
+
- **{{totalMeetings}}** meetings
|
|
3904
|
+
- **{{totalDuration | duration}}** total time
|
|
3905
|
+
- **{{actionItems.total}}** action items
|
|
3906
|
+
|
|
3907
|
+
## Meeting Stats
|
|
3908
|
+
{{#stats.meetingsByDay}}
|
|
3909
|
+
- {{.}}
|
|
3910
|
+
{{/stats.meetingsByDay}}
|
|
3911
|
+
|
|
3912
|
+
Busiest day: {{stats.busiestDay}}
|
|
3913
|
+
Average duration: {{stats.averageDuration}} minutes
|
|
3914
|
+
|
|
3915
|
+
## Action Items
|
|
3916
|
+
{{#actionItems.byAssignee}}
|
|
3917
|
+
### {{.}}
|
|
3918
|
+
{{/actionItems.byAssignee}}
|
|
3919
|
+
|
|
3920
|
+
{{#actionItems.unassigned}}
|
|
3921
|
+
### Unassigned
|
|
3922
|
+
- [ ] {{text}}
|
|
3923
|
+
{{/actionItems.unassigned}}
|
|
3924
|
+
|
|
3925
|
+
## Highlights
|
|
3926
|
+
{{#highlights}}
|
|
3927
|
+
### {{meetingTitle}} ({{meetingDate | date}})
|
|
3928
|
+
{{#keyPoints}}
|
|
3929
|
+
- {{.}}
|
|
3930
|
+
{{/keyPoints}}
|
|
3931
|
+
{{/highlights}}
|
|
3932
|
+
|
|
3933
|
+
## Participants
|
|
3934
|
+
{{#participants}}
|
|
3935
|
+
- {{email}} ({{meetingCount}} meetings, {{totalMinutes | duration}})
|
|
3936
|
+
{{/participants}}
|
|
3937
|
+
|
|
3938
|
+
---
|
|
3939
|
+
*Generated with fireflies-api*
|
|
3940
|
+
`;
|
|
3941
|
+
var COMPACT_TEMPLATE = `# Weekly Digest: {{period.from}} - {{period.to}}
|
|
3942
|
+
|
|
3943
|
+
**{{totalMeetings}}** meetings | **{{totalDuration | duration}}** | **{{actionItems.total}}** action items
|
|
3944
|
+
|
|
3945
|
+
{{#actionItems.unassigned}}
|
|
3946
|
+
## Action Items
|
|
3947
|
+
- [ ] {{text}}
|
|
3948
|
+
{{/actionItems.unassigned}}
|
|
3949
|
+
`;
|
|
3950
|
+
var EXECUTIVE_TEMPLATE = `# Executive Summary
|
|
3951
|
+
**Weekly Meeting Report: {{period.from}} to {{period.to}}**
|
|
3952
|
+
|
|
3953
|
+
## Key Metrics
|
|
3954
|
+
| Metric | Value |
|
|
3955
|
+
|--------|-------|
|
|
3956
|
+
| Total Meetings | {{totalMeetings}} |
|
|
3957
|
+
| Total Time | {{totalDuration | duration}} |
|
|
3958
|
+
| Action Items | {{actionItems.total}} |
|
|
3959
|
+
| Participants | {{participants.length}} |
|
|
3960
|
+
|
|
3961
|
+
## Top Highlights
|
|
3962
|
+
{{#highlights}}
|
|
3963
|
+
- **{{meetingTitle}}**: {{#keyPoints}}{{.}} {{/keyPoints}}
|
|
3964
|
+
{{/highlights}}
|
|
3965
|
+
|
|
3966
|
+
## Outstanding Action Items
|
|
3967
|
+
{{#actionItems.unassigned}}
|
|
3968
|
+
- {{text}}
|
|
3969
|
+
{{/actionItems.unassigned}}
|
|
3970
|
+
|
|
3971
|
+
---
|
|
3972
|
+
*Executive Summary generated with fireflies-api*
|
|
3973
|
+
`;
|
|
3974
|
+
|
|
3975
|
+
// src/cli/utils/date.ts
|
|
3976
|
+
function daysAgo(days) {
|
|
3977
|
+
const date = /* @__PURE__ */ new Date();
|
|
3978
|
+
date.setDate(date.getDate() - days);
|
|
3979
|
+
date.setHours(0, 0, 0, 0);
|
|
3980
|
+
return date.toISOString();
|
|
2946
3981
|
}
|
|
2947
|
-
function
|
|
2948
|
-
const
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
if (opts.includeTimestamps && firstSentence) {
|
|
2952
|
-
const timestamp = formatTimestamp(firstSentence.start_time);
|
|
2953
|
-
return `${timestamp} ${speaker} ${text}`;
|
|
2954
|
-
}
|
|
2955
|
-
return `${speaker} ${text}`;
|
|
3982
|
+
function startOfToday() {
|
|
3983
|
+
const date = /* @__PURE__ */ new Date();
|
|
3984
|
+
date.setHours(0, 0, 0, 0);
|
|
3985
|
+
return date.toISOString();
|
|
2956
3986
|
}
|
|
2957
|
-
function
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
const timestamp = formatTimestamp(sentence.start_time);
|
|
2961
|
-
return `${timestamp} ${speaker} ${sentence.text}`;
|
|
3987
|
+
function resolveDateRange(opts) {
|
|
3988
|
+
if (opts.today) {
|
|
3989
|
+
return { fromDate: startOfToday() };
|
|
2962
3990
|
}
|
|
2963
|
-
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3991
|
+
if (opts.yesterday) {
|
|
3992
|
+
return { fromDate: daysAgo(1), toDate: startOfToday() };
|
|
3993
|
+
}
|
|
3994
|
+
if (opts.lastWeek) {
|
|
3995
|
+
return { fromDate: daysAgo(7) };
|
|
3996
|
+
}
|
|
3997
|
+
if (opts.lastMonth) {
|
|
3998
|
+
return { fromDate: daysAgo(30) };
|
|
3999
|
+
}
|
|
4000
|
+
if (opts.days) {
|
|
4001
|
+
const numDays = Number.parseInt(opts.days, 10);
|
|
4002
|
+
if (!Number.isNaN(numDays) && numDays > 0) {
|
|
4003
|
+
return { fromDate: daysAgo(numDays) };
|
|
4004
|
+
}
|
|
2971
4005
|
}
|
|
4006
|
+
return { fromDate: opts.from, toDate: opts.to };
|
|
2972
4007
|
}
|
|
2973
|
-
function
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
return
|
|
4008
|
+
function createProgress(opts) {
|
|
4009
|
+
if (!opts.enabled || !process.stderr.isTTY) {
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
return ora({ text: opts.text, stream: process.stderr });
|
|
2978
4013
|
}
|
|
2979
|
-
function
|
|
2980
|
-
const
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
4014
|
+
async function withProgress(opts, task) {
|
|
4015
|
+
const spinner = createProgress(opts);
|
|
4016
|
+
spinner?.start();
|
|
4017
|
+
const update = (text) => {
|
|
4018
|
+
if (spinner) {
|
|
4019
|
+
spinner.text = text;
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
try {
|
|
4023
|
+
const result = await task(update);
|
|
4024
|
+
spinner?.succeed();
|
|
4025
|
+
return result;
|
|
4026
|
+
} catch (err) {
|
|
4027
|
+
spinner?.fail();
|
|
4028
|
+
throw err;
|
|
2984
4029
|
}
|
|
2985
|
-
return `${mins} minutes`;
|
|
2986
4030
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
4031
|
+
|
|
4032
|
+
// src/cli/commands/digest.ts
|
|
4033
|
+
function buildDigestFromTranscripts(transcripts, opts) {
|
|
4034
|
+
return buildDigest(transcripts, {
|
|
4035
|
+
includeActionItems: opts.actionItems !== false && !opts.statsOnly,
|
|
4036
|
+
includeHighlights: opts.highlights !== false && !opts.statsOnly,
|
|
4037
|
+
includeStats: true
|
|
2993
4038
|
});
|
|
2994
4039
|
}
|
|
2995
|
-
function
|
|
2996
|
-
if (
|
|
2997
|
-
return
|
|
4040
|
+
function renderDigestOutput(digest, outputFormat, template) {
|
|
4041
|
+
if (outputFormat === "json") {
|
|
4042
|
+
return JSON.stringify(digest, null, 2);
|
|
2998
4043
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
function parseMultilineField(value) {
|
|
3002
|
-
return value.split(/\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
3003
|
-
}
|
|
3004
|
-
async function writeIfOutputPath(content, outputPath) {
|
|
3005
|
-
if (outputPath) {
|
|
3006
|
-
const { writeFile: writeFile2 } = await import('fs/promises');
|
|
3007
|
-
await writeFile2(outputPath, content, "utf-8");
|
|
4044
|
+
if (outputFormat === "html") {
|
|
4045
|
+
return renderDigestHtml(digest);
|
|
3008
4046
|
}
|
|
4047
|
+
return renderDigest(digest, { template });
|
|
4048
|
+
}
|
|
4049
|
+
function registerDigestCommand(program2) {
|
|
4050
|
+
program2.command("digest").description("Generate a weekly meeting digest").option("--from <date>", "From date (YYYY-MM-DD or ISO 8601)").option("--to <date>", "To date (YYYY-MM-DD or ISO 8601)").option("--today", "Meetings from today").option("--yesterday", "Meetings from yesterday").option("--last-week", "Meetings from last 7 days").option("--last-month", "Meetings from last 30 days").option("--days <n>", "Meetings from last N days").option("--mine", "Only my transcripts").option("--external", "Only meetings with external (non-company) participants").option("--limit <n>", "Max transcripts to include").option(
|
|
4051
|
+
"--template <name>",
|
|
4052
|
+
"Template: default, compact, executive, or path to .md file",
|
|
4053
|
+
"default"
|
|
4054
|
+
).option("-o, --output-file <path>", "Write digest to file").option("--format <format>", "Output format: markdown, html, json (default: markdown)").option("--no-action-items", "Exclude action items section").option("--no-highlights", "Exclude highlights section").option("--stats-only", "Only show meeting statistics").action(
|
|
4055
|
+
withErrorHandling(async (opts) => {
|
|
4056
|
+
const client = getClient(program2);
|
|
4057
|
+
const format = getOutputFormat(program2);
|
|
4058
|
+
const showProgress = isProgressEnabled(program2);
|
|
4059
|
+
const { fromDate, toDate } = resolveDateRange(opts);
|
|
4060
|
+
if (!fromDate && !toDate) {
|
|
4061
|
+
writeLine("Error: Please specify a date range (--from, --to, --last-week, etc.)");
|
|
4062
|
+
process.exitCode = 1;
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const digestOutput = await withProgress(
|
|
4066
|
+
{ enabled: showProgress, text: "Generating digest..." },
|
|
4067
|
+
async (update) => {
|
|
4068
|
+
update("Fetching transcripts...");
|
|
4069
|
+
const transcripts = await client.transcripts.list({
|
|
4070
|
+
fromDate,
|
|
4071
|
+
toDate,
|
|
4072
|
+
mine: opts.mine,
|
|
4073
|
+
external: opts.external,
|
|
4074
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
|
|
4075
|
+
includeSummary: true
|
|
4076
|
+
});
|
|
4077
|
+
if (transcripts.length === 0) {
|
|
4078
|
+
return { digest: null, rendered: null, transcriptCount: 0 };
|
|
4079
|
+
}
|
|
4080
|
+
update(`Building digest from ${transcripts.length} transcripts...`);
|
|
4081
|
+
const digest = buildDigestFromTranscripts(transcripts, opts);
|
|
4082
|
+
update("Rendering output...");
|
|
4083
|
+
const outputFormat = opts.format || (format === "json" ? "json" : "markdown");
|
|
4084
|
+
const rendered = renderDigestOutput(digest, outputFormat, opts.template);
|
|
4085
|
+
return { digest, rendered, transcriptCount: transcripts.length };
|
|
4086
|
+
}
|
|
4087
|
+
);
|
|
4088
|
+
if (!digestOutput.digest || !digestOutput.rendered) {
|
|
4089
|
+
writeLine("No transcripts found for the specified date range.");
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
if (opts.outputFile) {
|
|
4093
|
+
writeFileSync(opts.outputFile, digestOutput.rendered);
|
|
4094
|
+
writeLine(
|
|
4095
|
+
`\u2713 Digest written to ${opts.outputFile} (${digestOutput.digest.actionItems.total} action items, ${digestOutput.transcriptCount} meetings)`
|
|
4096
|
+
);
|
|
4097
|
+
} else if (format === "json") {
|
|
4098
|
+
output(digestOutput.digest, "json");
|
|
4099
|
+
} else {
|
|
4100
|
+
writeLine(digestOutput.rendered);
|
|
4101
|
+
}
|
|
4102
|
+
})
|
|
4103
|
+
);
|
|
3009
4104
|
}
|
|
3010
|
-
|
|
3011
|
-
// src/cli/commands/export.ts
|
|
3012
4105
|
function registerExportCommand(program2) {
|
|
3013
4106
|
program2.command("export <transcript-id> [output-file]").description("Export transcript to markdown").option("--no-summary", "Exclude summary section").option("--no-timestamps", "Exclude timestamps").option("--format <format>", "Output format: markdown, json", "markdown").action(
|
|
3014
4107
|
withErrorHandling(async (transcriptId, outputFile, opts) => {
|
|
@@ -3037,39 +4130,137 @@ function registerExportCommand(program2) {
|
|
|
3037
4130
|
})
|
|
3038
4131
|
);
|
|
3039
4132
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
4133
|
+
function registerExportBulkCommand(program2) {
|
|
4134
|
+
program2.command("export-bulk").description("Export multiple transcripts to files").option("--from <date>", "From date (YYYY-MM-DD or ISO 8601)").option("--to <date>", "To date (YYYY-MM-DD or ISO 8601)").option("--today", "Meetings from today").option("--yesterday", "Meetings from yesterday").option("--last-week", "Meetings from last 7 days").option("--last-month", "Meetings from last 30 days").option("--days <n>", "Meetings from last N days").option("--ids <ids>", "Comma-separated transcript IDs").option("--mine", "Only my transcripts").option("--external", "Only meetings with external (non-company) participants").option("--limit <n>", "Max transcripts to export").requiredOption("-d, --dest <path>", "Output directory or .zip file").option(
|
|
4135
|
+
"--format <format>",
|
|
4136
|
+
"Export format: markdown, json, txt, csv (default: markdown)",
|
|
4137
|
+
"markdown"
|
|
4138
|
+
).option("--zip", "Package as zip archive").option("--dry-run", "Show what would be exported without writing files").action(
|
|
4139
|
+
withErrorHandling(async (opts) => {
|
|
4140
|
+
const client = getClient(program2);
|
|
4141
|
+
const showProgress = isProgressEnabled(program2);
|
|
4142
|
+
const exportOpts = parseExportOptions(opts);
|
|
4143
|
+
if (!exportOpts) return;
|
|
4144
|
+
if (opts.dryRun) {
|
|
4145
|
+
await runDryMode(client, exportOpts);
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
await runExport(client, exportOpts, showProgress);
|
|
4149
|
+
})
|
|
4150
|
+
);
|
|
3052
4151
|
}
|
|
3053
|
-
function
|
|
3054
|
-
|
|
3055
|
-
|
|
4152
|
+
function parseExportOptions(opts) {
|
|
4153
|
+
const { fromDate, toDate } = resolveDateRange(opts);
|
|
4154
|
+
const format = validateFormat(opts.format);
|
|
4155
|
+
const outputPath = opts.dest;
|
|
4156
|
+
const asZip = Boolean(opts.zip) || outputPath.endsWith(".zip");
|
|
4157
|
+
const ids = opts.ids ? opts.ids.split(",").map((id) => id.trim()) : void 0;
|
|
4158
|
+
if (!fromDate && !toDate && !ids) {
|
|
4159
|
+
writeLine("Error: Please specify a date range (--from, --to, --last-week, etc.) or --ids");
|
|
4160
|
+
process.exitCode = 1;
|
|
4161
|
+
return null;
|
|
3056
4162
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
4163
|
+
return {
|
|
4164
|
+
fromDate,
|
|
4165
|
+
toDate,
|
|
4166
|
+
ids,
|
|
4167
|
+
mine: opts.mine,
|
|
4168
|
+
external: opts.external,
|
|
4169
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
|
|
4170
|
+
format,
|
|
4171
|
+
asZip,
|
|
4172
|
+
outputPath
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
async function runExport(client, opts, showProgress) {
|
|
4176
|
+
const result = await withProgress(
|
|
4177
|
+
{ enabled: showProgress, text: "Exporting transcripts..." },
|
|
4178
|
+
async (update) => {
|
|
4179
|
+
return client.transcripts.bulkExport({
|
|
4180
|
+
fromDate: opts.fromDate,
|
|
4181
|
+
toDate: opts.toDate,
|
|
4182
|
+
ids: opts.ids,
|
|
4183
|
+
mine: opts.mine,
|
|
4184
|
+
external: opts.external,
|
|
4185
|
+
limit: opts.limit,
|
|
4186
|
+
format: opts.format,
|
|
4187
|
+
asZip: opts.asZip,
|
|
4188
|
+
onProgress: (completed, total) => {
|
|
4189
|
+
update(`Exporting transcripts... ${completed}/${total}`);
|
|
4190
|
+
}
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
);
|
|
4194
|
+
await writeExportResult(result, opts);
|
|
4195
|
+
}
|
|
4196
|
+
async function writeExportResult(result, opts) {
|
|
4197
|
+
if (result.totalExported === 0) {
|
|
4198
|
+
writeLine("No transcripts found matching the criteria.");
|
|
4199
|
+
return;
|
|
3059
4200
|
}
|
|
3060
|
-
if (opts.
|
|
3061
|
-
|
|
4201
|
+
if (opts.asZip && result.zip) {
|
|
4202
|
+
await writeFile(opts.outputPath, result.zip);
|
|
4203
|
+
writeLine(`\u2713 Exported ${result.totalExported} transcripts to ${opts.outputPath}`);
|
|
4204
|
+
} else {
|
|
4205
|
+
await mkdir(opts.outputPath, { recursive: true });
|
|
4206
|
+
for (const file of result.files) {
|
|
4207
|
+
await writeFile(join(opts.outputPath, file.filename), file.content);
|
|
4208
|
+
}
|
|
4209
|
+
writeLine(`\u2713 Exported ${result.totalExported} transcripts to ${opts.outputPath}/`);
|
|
3062
4210
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
4211
|
+
}
|
|
4212
|
+
function validateFormat(format) {
|
|
4213
|
+
const validFormats = ["markdown", "json", "txt", "csv"];
|
|
4214
|
+
if (!validFormats.includes(format)) {
|
|
4215
|
+
throw new Error(`Invalid format "${format}". Valid formats: ${validFormats.join(", ")}`);
|
|
3065
4216
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
4217
|
+
return format;
|
|
4218
|
+
}
|
|
4219
|
+
async function runDryMode(client, params) {
|
|
4220
|
+
writeLine("Dry run - no files will be written\n");
|
|
4221
|
+
const transcripts = await collectDryRunTranscripts(client, params);
|
|
4222
|
+
printDryRunResults(transcripts, params);
|
|
4223
|
+
}
|
|
4224
|
+
async function collectDryRunTranscripts(client, params) {
|
|
4225
|
+
const transcripts = [];
|
|
4226
|
+
if (params.ids?.length) {
|
|
4227
|
+
for (const id of params.ids) {
|
|
4228
|
+
try {
|
|
4229
|
+
const t = await client.transcripts.get(id, {
|
|
4230
|
+
includeSentences: false,
|
|
4231
|
+
includeSummary: false
|
|
4232
|
+
});
|
|
4233
|
+
transcripts.push({ id: t.id, title: t.title, date: t.dateString || "Unknown" });
|
|
4234
|
+
} catch {
|
|
4235
|
+
writeLine(` \u26A0 Transcript ${id} not found`);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
} else {
|
|
4239
|
+
for await (const t of client.transcripts.listAll({
|
|
4240
|
+
fromDate: params.fromDate,
|
|
4241
|
+
toDate: params.toDate,
|
|
4242
|
+
mine: params.mine
|
|
4243
|
+
})) {
|
|
4244
|
+
transcripts.push({ id: t.id, title: t.title, date: t.dateString || "Unknown" });
|
|
4245
|
+
if (params.limit && transcripts.length >= params.limit) break;
|
|
3070
4246
|
}
|
|
3071
4247
|
}
|
|
3072
|
-
return
|
|
4248
|
+
return transcripts;
|
|
4249
|
+
}
|
|
4250
|
+
function printDryRunResults(transcripts, params) {
|
|
4251
|
+
if (transcripts.length === 0) {
|
|
4252
|
+
writeLine("No transcripts found matching the criteria.");
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
writeLine(`Would export ${transcripts.length} transcripts:
|
|
4256
|
+
`);
|
|
4257
|
+
for (const t of transcripts) {
|
|
4258
|
+
const dateStr = t.date !== "Unknown" ? new Date(t.date).toLocaleDateString() : "Unknown";
|
|
4259
|
+
writeLine(` ${dateStr} - ${t.title}`);
|
|
4260
|
+
}
|
|
4261
|
+
writeLine("");
|
|
4262
|
+
writeLine(`Format: ${params.format}`);
|
|
4263
|
+
writeLine(`Output: ${params.outputPath}${params.asZip ? "" : "/"}`);
|
|
3073
4264
|
}
|
|
3074
4265
|
|
|
3075
4266
|
// src/cli/commands/insights.ts
|
|
@@ -3081,9 +4272,11 @@ function registerInsightsCommand(program2) {
|
|
|
3081
4272
|
withErrorHandling(async (opts) => {
|
|
3082
4273
|
const client = getClient(program2);
|
|
3083
4274
|
const format = getOutputFormat(program2);
|
|
4275
|
+
const showProgress = isProgressEnabled(program2);
|
|
3084
4276
|
const { fromDate, toDate } = resolveDateRange(opts);
|
|
3085
|
-
const insights = await
|
|
3086
|
-
|
|
4277
|
+
const insights = await withProgress(
|
|
4278
|
+
{ enabled: showProgress, text: "Analyzing meetings..." },
|
|
4279
|
+
async () => client.transcripts.insights(buildInsightsParams(opts, fromDate, toDate))
|
|
3087
4280
|
);
|
|
3088
4281
|
outputInsights(insights, format);
|
|
3089
4282
|
})
|
|
@@ -3134,7 +4327,7 @@ function outputHeader(insights) {
|
|
|
3134
4327
|
}
|
|
3135
4328
|
function outputSummaryStats(insights) {
|
|
3136
4329
|
writeLine(`Total meetings: ${insights.totalMeetings}`);
|
|
3137
|
-
writeLine(`Total duration: ${
|
|
4330
|
+
writeLine(`Total duration: ${formatDuration2(insights.totalDurationMinutes * 60)}`);
|
|
3138
4331
|
writeLine(`Average duration: ${Math.round(insights.averageDurationMinutes)} min`);
|
|
3139
4332
|
writeLine("");
|
|
3140
4333
|
}
|
|
@@ -3143,7 +4336,7 @@ function outputDayOfWeekStats(byDayOfWeek) {
|
|
|
3143
4336
|
const sortedDays = getSortedDays(byDayOfWeek);
|
|
3144
4337
|
for (const { day, stats } of sortedDays) {
|
|
3145
4338
|
if (stats.count > 0) {
|
|
3146
|
-
const duration =
|
|
4339
|
+
const duration = formatDuration2(stats.totalMinutes * 60);
|
|
3147
4340
|
writeLine(` ${capitalize(day)}: ${stats.count} meetings (${duration})`);
|
|
3148
4341
|
}
|
|
3149
4342
|
}
|
|
@@ -3153,7 +4346,7 @@ function outputTimeGroupStats(byTimeGroup) {
|
|
|
3153
4346
|
if (!byTimeGroup || byTimeGroup.length === 0) return;
|
|
3154
4347
|
writeLine("By Period:");
|
|
3155
4348
|
for (const group of byTimeGroup) {
|
|
3156
|
-
const duration =
|
|
4349
|
+
const duration = formatDuration2(group.totalMinutes * 60);
|
|
3157
4350
|
const avg = Math.round(group.averageMinutes);
|
|
3158
4351
|
writeLine(` ${group.period}: ${group.count} meetings (${duration}, avg ${avg} min)`);
|
|
3159
4352
|
}
|
|
@@ -3189,7 +4382,7 @@ function outputTopSpeakers(speakers) {
|
|
|
3189
4382
|
for (let i = 0; i < speakers.length; i++) {
|
|
3190
4383
|
const s = speakers[i];
|
|
3191
4384
|
if (s) {
|
|
3192
|
-
const talkTime =
|
|
4385
|
+
const talkTime = formatDuration2(s.totalTalkTimeSeconds);
|
|
3193
4386
|
writeLine(` ${i + 1}. ${s.name} (${s.meetingCount} meetings, ${talkTime} talk time)`);
|
|
3194
4387
|
}
|
|
3195
4388
|
}
|
|
@@ -3197,7 +4390,7 @@ function outputTopSpeakers(speakers) {
|
|
|
3197
4390
|
function outputInsightsTable(insights) {
|
|
3198
4391
|
const summary = {
|
|
3199
4392
|
totalMeetings: insights.totalMeetings,
|
|
3200
|
-
totalDuration:
|
|
4393
|
+
totalDuration: formatDuration2(insights.totalDurationMinutes * 60),
|
|
3201
4394
|
avgDuration: `${Math.round(insights.averageDurationMinutes)} min`,
|
|
3202
4395
|
dateRange: `${insights.earliestMeeting} to ${insights.latestMeeting}`,
|
|
3203
4396
|
uniqueParticipants: insights.totalUniqueParticipants,
|
|
@@ -3372,21 +4565,25 @@ function registerSearchCommand(program2) {
|
|
|
3372
4565
|
withErrorHandling(async (query, opts) => {
|
|
3373
4566
|
const client = getClient(program2);
|
|
3374
4567
|
const format = getOutputFormat(program2);
|
|
4568
|
+
const showProgress = isProgressEnabled(program2);
|
|
3375
4569
|
const { fromDate, toDate } = resolveDateRange(opts);
|
|
3376
|
-
const results = await
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
4570
|
+
const results = await withProgress(
|
|
4571
|
+
{ enabled: showProgress, text: `Searching for "${query}"...` },
|
|
4572
|
+
async () => client.transcripts.search(query, {
|
|
4573
|
+
caseSensitive: opts.caseSensitive,
|
|
4574
|
+
scope: opts.scope,
|
|
4575
|
+
speakers: opts.speaker.length > 0 ? opts.speaker : void 0,
|
|
4576
|
+
filterQuestions: opts.questions,
|
|
4577
|
+
filterTasks: opts.tasks,
|
|
4578
|
+
contextLines: Number.parseInt(opts.context, 10),
|
|
4579
|
+
fromDate,
|
|
4580
|
+
toDate,
|
|
4581
|
+
mine: opts.mine,
|
|
4582
|
+
organizers: opts.organizer.length > 0 ? opts.organizer : void 0,
|
|
4583
|
+
participants: opts.participant.length > 0 ? opts.participant : void 0,
|
|
4584
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0
|
|
4585
|
+
})
|
|
4586
|
+
);
|
|
3390
4587
|
outputSearchResults(results, format);
|
|
3391
4588
|
})
|
|
3392
4589
|
);
|
|
@@ -3701,28 +4898,35 @@ function arrayOrUndefined(arr) {
|
|
|
3701
4898
|
}
|
|
3702
4899
|
function registerTranscriptsCommand(program2) {
|
|
3703
4900
|
const cmd = program2.command("transcripts").description("Manage transcripts");
|
|
3704
|
-
cmd.command("list").description("List transcripts").option("--limit <n>", "Max results (default: 20)", "20").option("--from <date>", "From date (YYYY-MM-DD or ISO 8601)").option("--to <date>", "To date (YYYY-MM-DD or ISO 8601)").option("--today", "Transcripts from today").option("--yesterday", "Transcripts from yesterday").option("--last-week", "Transcripts from last 7 days").option("--last-month", "Transcripts from last 30 days").option("--days <n>", "Transcripts from last N days").option("--mine", "Only my transcripts").option("--keyword <text>", "Search keyword").option("--scope <scope>", "Search scope: title, sentences, all (default: all)").option("--organizer <email>", "Filter by organizer email (repeatable)", collect3, []).option("--participant <email>", "Filter by participant email (repeatable)", collect3, []).option("--participant-me", "Only meetings where I am a participant").option("--user-id <id>", "Filter by user ID").option("--channel <id>", "Filter by channel ID").option("--normalize", "Output in normalized provider-agnostic format").action(
|
|
4901
|
+
cmd.command("list").description("List transcripts").option("--limit <n>", "Max results (default: 20)", "20").option("--from <date>", "From date (YYYY-MM-DD or ISO 8601)").option("--to <date>", "To date (YYYY-MM-DD or ISO 8601)").option("--today", "Transcripts from today").option("--yesterday", "Transcripts from yesterday").option("--last-week", "Transcripts from last 7 days").option("--last-month", "Transcripts from last 30 days").option("--days <n>", "Transcripts from last N days").option("--mine", "Only my transcripts").option("--keyword <text>", "Search keyword").option("--scope <scope>", "Search scope: title, sentences, all (default: all)").option("--organizer <email>", "Filter by organizer email (repeatable)", collect3, []).option("--participant <email>", "Filter by participant email (repeatable)", collect3, []).option("--participant-me", "Only meetings where I am a participant").option("--user-id <id>", "Filter by user ID").option("--channel <id>", "Filter by channel ID").option("--external", "Only meetings with external (non-company) participants").option("--include-sentences", "Include full transcript sentences (large response)").option("--include-summary", "Include summary with action items, keywords, etc.").option("--normalize", "Output in normalized provider-agnostic format").action(
|
|
3705
4902
|
withErrorHandling(async (opts) => {
|
|
3706
4903
|
const client = getClient(program2);
|
|
3707
4904
|
const format = getOutputFormat(program2);
|
|
4905
|
+
const showProgress = isProgressEnabled(program2);
|
|
3708
4906
|
const { fromDate, toDate } = resolveDateRange(opts);
|
|
3709
4907
|
const participants = [...opts.participant];
|
|
3710
4908
|
if (opts.participantMe) {
|
|
3711
4909
|
const me = await client.users.me();
|
|
3712
4910
|
participants.push(me.email);
|
|
3713
4911
|
}
|
|
3714
|
-
const transcripts = await
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
4912
|
+
const transcripts = await withProgress(
|
|
4913
|
+
{ enabled: showProgress, text: "Fetching transcripts..." },
|
|
4914
|
+
async () => client.transcripts.list({
|
|
4915
|
+
limit: Number.parseInt(opts.limit, 10),
|
|
4916
|
+
fromDate,
|
|
4917
|
+
toDate,
|
|
4918
|
+
mine: opts.mine,
|
|
4919
|
+
keyword: opts.keyword,
|
|
4920
|
+
scope: opts.scope,
|
|
4921
|
+
organizers: opts.organizer.length > 0 ? opts.organizer : void 0,
|
|
4922
|
+
participants: participants.length > 0 ? participants : void 0,
|
|
4923
|
+
user_id: opts.userId,
|
|
4924
|
+
channel_id: opts.channel,
|
|
4925
|
+
external: opts.external,
|
|
4926
|
+
includeSentences: opts.includeSentences,
|
|
4927
|
+
includeSummary: opts.includeSummary
|
|
4928
|
+
})
|
|
4929
|
+
);
|
|
3726
4930
|
if (opts.normalize) {
|
|
3727
4931
|
const fullTranscripts = await Promise.all(
|
|
3728
4932
|
transcripts.map((t) => client.transcripts.get(t.id))
|
|
@@ -3731,12 +4935,16 @@ function registerTranscriptsCommand(program2) {
|
|
|
3731
4935
|
output(normalized, format);
|
|
3732
4936
|
return;
|
|
3733
4937
|
}
|
|
4938
|
+
if (opts.includeSentences || opts.includeSummary) {
|
|
4939
|
+
output(transcripts, format);
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
3734
4942
|
const useHumanDuration = format === "table" || format === "plain";
|
|
3735
4943
|
const formatted = transcripts.map((t) => ({
|
|
3736
4944
|
id: t.id,
|
|
3737
4945
|
title: t.title,
|
|
3738
4946
|
date: t.dateString,
|
|
3739
|
-
duration: useHumanDuration ?
|
|
4947
|
+
duration: useHumanDuration ? formatDuration2(t.duration * 60) : Math.round(t.duration),
|
|
3740
4948
|
organizer: t.organizer_email
|
|
3741
4949
|
}));
|
|
3742
4950
|
output(formatted, format);
|
|
@@ -3797,17 +5005,21 @@ function registerTranscriptsCommand(program2) {
|
|
|
3797
5005
|
withErrorHandling(async (opts) => {
|
|
3798
5006
|
const client = getClient(program2);
|
|
3799
5007
|
const format = getOutputFormat(program2);
|
|
5008
|
+
const showProgress = isProgressEnabled(program2);
|
|
3800
5009
|
const { fromDate, toDate } = resolveDateRange(opts);
|
|
3801
5010
|
const filterOptions = buildActionItemFilterOptions(opts);
|
|
3802
|
-
const result = await
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
5011
|
+
const result = await withProgress(
|
|
5012
|
+
{ enabled: showProgress, text: "Exporting action items..." },
|
|
5013
|
+
async () => client.transcripts.exportActionItems({
|
|
5014
|
+
fromDate,
|
|
5015
|
+
toDate,
|
|
5016
|
+
mine: opts.mine,
|
|
5017
|
+
organizers: arrayOrUndefined(opts.organizer),
|
|
5018
|
+
participants: arrayOrUndefined(opts.participant),
|
|
5019
|
+
limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
|
|
5020
|
+
filterOptions
|
|
5021
|
+
})
|
|
5022
|
+
);
|
|
3811
5023
|
if (format === "plain") {
|
|
3812
5024
|
const markdown = formatActionItemsMarkdown(result, buildMarkdownOptions(opts));
|
|
3813
5025
|
return opts.output ? writeToFile(opts.output, markdown, result.totalItems) : writeLine(markdown);
|
|
@@ -3890,10 +5102,11 @@ function getVersion() {
|
|
|
3890
5102
|
}
|
|
3891
5103
|
}
|
|
3892
5104
|
var program = new Command();
|
|
3893
|
-
program.name("fireflies").description("CLI for Fireflies.ai API").version(getVersion()).option("-k, --api-key <key>", "API key (or FIREFLIES_API_KEY env)").option("-o, --output <format>", "Output format: json, jsonl, table, tsv, plain", "json");
|
|
5105
|
+
program.name("fireflies").description("CLI for Fireflies.ai API").version(getVersion()).option("-k, --api-key <key>", "API key (or FIREFLIES_API_KEY env)").option("-o, --output <format>", "Output format: json, jsonl, table, tsv, plain", "json").option("--progress", "Show progress indicators during long operations");
|
|
3894
5106
|
registerTranscriptsCommand(program);
|
|
3895
5107
|
registerSearchCommand(program);
|
|
3896
5108
|
registerInsightsCommand(program);
|
|
5109
|
+
registerDigestCommand(program);
|
|
3897
5110
|
registerMeetingsCommand(program);
|
|
3898
5111
|
registerUsersCommand(program);
|
|
3899
5112
|
registerBitesCommand(program);
|
|
@@ -3901,6 +5114,7 @@ registerAiAppsCommand(program);
|
|
|
3901
5114
|
registerAudioCommand(program);
|
|
3902
5115
|
registerRealtimeCommand(program);
|
|
3903
5116
|
registerExportCommand(program);
|
|
5117
|
+
registerExportBulkCommand(program);
|
|
3904
5118
|
program.parse();
|
|
3905
5119
|
//# sourceMappingURL=index.js.map
|
|
3906
5120
|
//# sourceMappingURL=index.js.map
|