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
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 { writeFile } from 'fs/promises';
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 delay = calculateDelay(error, attempt, baseDelay, maxDelay);
286
- await sleep(delay);
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 delay = this.rateLimitTracker.getThrottleDelay(this.rateLimitConfig.throttle);
436
- if (delay > 0) {
437
- await sleep2(delay);
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
- ${TRANSCRIPT_LIST_FIELDS}
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
- return data.transcripts.map(normalizeTranscript);
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
- ...listParams
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 full = await this.get(t.id, { includeSentences: true });
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
- const full = await this.get(t.id, { includeSentences: true, includeSummary: false });
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
- const full = await this.get(t.id, { includeSentences: false, includeSummary: true });
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 formatDuration(seconds) {
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/markdown.ts
2849
- var DEFAULT_OPTIONS2 = {
2850
- includeMetadata: true,
2851
- includeSummary: true,
2852
- includeActionItems: true,
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
- if (opts.includeSummary && transcript.summary) {
2865
- sections.push(formatSummary(transcript.summary, opts));
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
- if (transcript.sentences && transcript.sentences.length > 0) {
2868
- sections.push(formatTranscript(transcript.sentences, opts));
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
- const content = sections.join("\n\n---\n\n");
2871
- await writeIfOutputPath(content, options.outputPath);
2872
- return content;
3346
+ return busiestDay;
2873
3347
  }
2874
- function formatMetadata(transcript) {
2875
- const lines = [`# ${transcript.title || "Untitled Meeting"}`];
2876
- if (transcript.dateString) {
2877
- lines.push(`
2878
- **Date:** ${formatDate(transcript.dateString)}`);
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
- const duration = calculateDuration(transcript);
2881
- if (duration > 0) {
2882
- lines.push(`**Duration:** ${formatDuration2(duration)}`);
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
- const participants = getParticipantNames(transcript);
2885
- if (participants.length > 0) {
2886
- lines.push(`**Participants:** ${participants.join(", ")}`);
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 lines.join("\n");
3400
+ return namesByEmail;
2889
3401
  }
2890
- function calculateDuration(transcript) {
2891
- if (transcript.sentences && transcript.sentences.length > 0) {
2892
- const lastSentence = transcript.sentences[transcript.sentences.length - 1];
2893
- if (lastSentence) {
2894
- return parseFloat(lastSentence.end_time);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 transcript.duration || 0;
3840
+ return result;
2898
3841
  }
2899
- function formatSummary(summary, opts) {
2900
- const sections = ["## Summary"];
2901
- if (summary.gist) {
2902
- sections.push("", summary.gist);
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
- if (summary.bullet_gist) {
2905
- const bullets = parseMultilineField(summary.bullet_gist);
2906
- if (bullets.length > 0) {
2907
- sections.push("", "### Key Points");
2908
- sections.push(bullets.map((p) => `- ${p}`).join("\n"));
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
- if (opts.includeActionItems && summary.action_items) {
2912
- const items = parseMultilineField(summary.action_items);
2913
- if (items.length > 0) {
2914
- sections.push("", "### Action Items");
2915
- const prefix = opts.actionItemFormat === "checkbox" ? "- [ ] " : "- ";
2916
- sections.push(items.map((a) => `${prefix}${a}`).join("\n"));
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 formatTranscript(sentences, opts) {
2922
- const lines = ["## Transcript"];
2923
- if (opts.groupBySpeaker) {
2924
- const groups = groupSentencesBySpeaker(sentences);
2925
- for (const group of groups) {
2926
- lines.push("", formatSpeakerGroup(group, opts));
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
- } else {
2929
- for (const sentence of sentences) {
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 lines.join("\n");
3897
+ return current;
2934
3898
  }
2935
- function groupSentencesBySpeaker(sentences) {
2936
- const groups = [];
2937
- let current = null;
2938
- for (const sentence of sentences) {
2939
- if (!current || current.speakerName !== sentence.speaker_name) {
2940
- current = { speakerName: sentence.speaker_name, sentences: [] };
2941
- groups.push(current);
2942
- }
2943
- current.sentences.push(sentence);
2944
- }
2945
- return groups;
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 formatSpeakerGroup(group, opts) {
2948
- const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
2949
- const text = group.sentences.map((s) => s.text).join(" ");
2950
- const firstSentence = group.sentences[0];
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 formatSentence(sentence, opts) {
2958
- const speaker = formatSpeakerName(sentence.speaker_name, opts.speakerFormat);
2959
- if (opts.includeTimestamps) {
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
- return `${speaker} ${sentence.text}`;
2964
- }
2965
- function formatSpeakerName(name, format) {
2966
- switch (format) {
2967
- case "bold":
2968
- return `**${name}:**`;
2969
- case "plain":
2970
- return `${name}:`;
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 formatTimestamp(startTime) {
2974
- const seconds = parseFloat(startTime);
2975
- const mins = Math.floor(seconds / 60);
2976
- const secs = Math.floor(seconds % 60);
2977
- return `[${mins}:${secs.toString().padStart(2, "0")}]`;
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 formatDuration2(seconds) {
2980
- const hours = Math.floor(seconds / 3600);
2981
- const mins = Math.floor(seconds % 3600 / 60);
2982
- if (hours > 0) {
2983
- return `${hours}h ${mins}m`;
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
- function formatDate(isoString) {
2988
- return new Date(isoString).toLocaleDateString("en-US", {
2989
- weekday: "long",
2990
- year: "numeric",
2991
- month: "long",
2992
- day: "numeric"
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 getParticipantNames(transcript) {
2996
- if (transcript.meeting_attendees?.length) {
2997
- return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
4040
+ function renderDigestOutput(digest, outputFormat, template) {
4041
+ if (outputFormat === "json") {
4042
+ return JSON.stringify(digest, null, 2);
2998
4043
  }
2999
- return transcript.speakers?.map((s) => s.name) || [];
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
- // src/cli/utils/date.ts
3042
- function daysAgo(days) {
3043
- const date = /* @__PURE__ */ new Date();
3044
- date.setDate(date.getDate() - days);
3045
- date.setHours(0, 0, 0, 0);
3046
- return date.toISOString();
3047
- }
3048
- function startOfToday() {
3049
- const date = /* @__PURE__ */ new Date();
3050
- date.setHours(0, 0, 0, 0);
3051
- return date.toISOString();
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 resolveDateRange(opts) {
3054
- if (opts.today) {
3055
- return { fromDate: startOfToday() };
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
- if (opts.yesterday) {
3058
- return { fromDate: daysAgo(1), toDate: startOfToday() };
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.lastWeek) {
3061
- return { fromDate: daysAgo(7) };
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
- if (opts.lastMonth) {
3064
- return { fromDate: daysAgo(30) };
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
- if (opts.days) {
3067
- const numDays = Number.parseInt(opts.days, 10);
3068
- if (!Number.isNaN(numDays) && numDays > 0) {
3069
- return { fromDate: daysAgo(numDays) };
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 { fromDate: opts.from, toDate: opts.to };
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 client.transcripts.insights(
3086
- buildInsightsParams(opts, fromDate, toDate)
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: ${formatDuration(insights.totalDurationMinutes * 60)}`);
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 = formatDuration(stats.totalMinutes * 60);
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 = formatDuration(group.totalMinutes * 60);
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 = formatDuration(s.totalTalkTimeSeconds);
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: formatDuration(insights.totalDurationMinutes * 60),
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 client.transcripts.search(query, {
3377
- caseSensitive: opts.caseSensitive,
3378
- scope: opts.scope,
3379
- speakers: opts.speaker.length > 0 ? opts.speaker : void 0,
3380
- filterQuestions: opts.questions,
3381
- filterTasks: opts.tasks,
3382
- contextLines: Number.parseInt(opts.context, 10),
3383
- fromDate,
3384
- toDate,
3385
- mine: opts.mine,
3386
- organizers: opts.organizer.length > 0 ? opts.organizer : void 0,
3387
- participants: opts.participant.length > 0 ? opts.participant : void 0,
3388
- limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0
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 client.transcripts.list({
3715
- limit: Number.parseInt(opts.limit, 10),
3716
- fromDate,
3717
- toDate,
3718
- mine: opts.mine,
3719
- keyword: opts.keyword,
3720
- scope: opts.scope,
3721
- organizers: opts.organizer.length > 0 ? opts.organizer : void 0,
3722
- participants: participants.length > 0 ? participants : void 0,
3723
- user_id: opts.userId,
3724
- channel_id: opts.channel
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 ? formatDuration(t.duration * 60) : Math.round(t.duration),
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 client.transcripts.exportActionItems({
3803
- fromDate,
3804
- toDate,
3805
- mine: opts.mine,
3806
- organizers: arrayOrUndefined(opts.organizer),
3807
- participants: arrayOrUndefined(opts.participant),
3808
- limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
3809
- filterOptions
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