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