fireflies-api 0.5.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/dist/action-items-CC9yUxHY.d.cts +380 -0
  4. package/dist/action-items-CC9yUxHY.d.ts +380 -0
  5. package/dist/cli/index.cjs +3909 -0
  6. package/dist/cli/index.cjs.map +1 -0
  7. package/dist/cli/index.d.cts +2 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/index.js +3906 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/index.cjs +3389 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +966 -0
  14. package/dist/index.d.ts +966 -0
  15. package/dist/index.js +3344 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/express.cjs +2491 -0
  18. package/dist/middleware/express.cjs.map +1 -0
  19. package/dist/middleware/express.d.cts +36 -0
  20. package/dist/middleware/express.d.ts +36 -0
  21. package/dist/middleware/express.js +2489 -0
  22. package/dist/middleware/express.js.map +1 -0
  23. package/dist/middleware/fastify.cjs +2501 -0
  24. package/dist/middleware/fastify.cjs.map +1 -0
  25. package/dist/middleware/fastify.d.cts +66 -0
  26. package/dist/middleware/fastify.d.ts +66 -0
  27. package/dist/middleware/fastify.js +2498 -0
  28. package/dist/middleware/fastify.js.map +1 -0
  29. package/dist/middleware/hono.cjs +2493 -0
  30. package/dist/middleware/hono.cjs.map +1 -0
  31. package/dist/middleware/hono.d.cts +37 -0
  32. package/dist/middleware/hono.d.ts +37 -0
  33. package/dist/middleware/hono.js +2490 -0
  34. package/dist/middleware/hono.js.map +1 -0
  35. package/dist/schemas/index.cjs +307 -0
  36. package/dist/schemas/index.cjs.map +1 -0
  37. package/dist/schemas/index.d.cts +926 -0
  38. package/dist/schemas/index.d.ts +926 -0
  39. package/dist/schemas/index.js +268 -0
  40. package/dist/schemas/index.js.map +1 -0
  41. package/dist/speaker-analytics-Dr46LKyP.d.ts +275 -0
  42. package/dist/speaker-analytics-l45LXqO1.d.cts +275 -0
  43. package/dist/types-BX-3JcRI.d.cts +41 -0
  44. package/dist/types-C_XxdRd1.d.cts +1546 -0
  45. package/dist/types-CaHcwnKw.d.ts +41 -0
  46. package/dist/types-DIPZmUl3.d.ts +1546 -0
  47. package/package.json +126 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,3389 @@
1
+ 'use strict';
2
+
3
+ var socket_ioClient = require('socket.io-client');
4
+ var crypto = require('crypto');
5
+
6
+ // src/errors.ts
7
+ var FirefliesError = class extends Error {
8
+ code = "FIREFLIES_ERROR";
9
+ status;
10
+ constructor(message, options) {
11
+ super(message, { cause: options?.cause });
12
+ this.name = "FirefliesError";
13
+ this.status = options?.status;
14
+ }
15
+ };
16
+ var AuthenticationError = class extends FirefliesError {
17
+ code = "AUTHENTICATION_ERROR";
18
+ constructor(message = "Invalid or missing API key") {
19
+ super(message, { status: 401 });
20
+ this.name = "AuthenticationError";
21
+ }
22
+ };
23
+ var RateLimitError = class extends FirefliesError {
24
+ code = "RATE_LIMIT_ERROR";
25
+ /** Suggested wait time in milliseconds before retrying. */
26
+ retryAfter;
27
+ constructor(message = "Rate limit exceeded", retryAfter) {
28
+ super(message, { status: 429 });
29
+ this.name = "RateLimitError";
30
+ this.retryAfter = retryAfter;
31
+ }
32
+ };
33
+ var NotFoundError = class extends FirefliesError {
34
+ code = "NOT_FOUND";
35
+ constructor(message = "Resource not found") {
36
+ super(message, { status: 404 });
37
+ this.name = "NotFoundError";
38
+ }
39
+ };
40
+ var ValidationError = class extends FirefliesError {
41
+ code = "VALIDATION_ERROR";
42
+ constructor(message) {
43
+ super(message, { status: 400 });
44
+ this.name = "ValidationError";
45
+ }
46
+ };
47
+ var GraphQLError = class extends FirefliesError {
48
+ code = "GRAPHQL_ERROR";
49
+ errors;
50
+ constructor(message, errors) {
51
+ super(message);
52
+ this.name = "GraphQLError";
53
+ this.errors = errors;
54
+ }
55
+ };
56
+ var TimeoutError = class extends FirefliesError {
57
+ code = "TIMEOUT_ERROR";
58
+ constructor(message = "Request timed out") {
59
+ super(message, { status: 408 });
60
+ this.name = "TimeoutError";
61
+ }
62
+ };
63
+ var NetworkError = class extends FirefliesError {
64
+ code = "NETWORK_ERROR";
65
+ constructor(message, cause) {
66
+ super(message, { cause });
67
+ this.name = "NetworkError";
68
+ }
69
+ };
70
+ var RealtimeError = class extends FirefliesError {
71
+ code = "REALTIME_ERROR";
72
+ constructor(message, options) {
73
+ super(message, options);
74
+ this.name = "RealtimeError";
75
+ }
76
+ };
77
+ var ConnectionError = class extends RealtimeError {
78
+ code = "CONNECTION_ERROR";
79
+ constructor(message = "Failed to establish realtime connection", options) {
80
+ super(message, options);
81
+ this.name = "ConnectionError";
82
+ }
83
+ };
84
+ var StreamClosedError = class extends RealtimeError {
85
+ code = "STREAM_CLOSED";
86
+ constructor(message = "Stream has been closed") {
87
+ super(message);
88
+ this.name = "StreamClosedError";
89
+ }
90
+ };
91
+ var WebhookVerificationError = class extends FirefliesError {
92
+ code = "WEBHOOK_VERIFICATION_FAILED";
93
+ constructor(message) {
94
+ super(message, { status: 401 });
95
+ this.name = "WebhookVerificationError";
96
+ }
97
+ };
98
+ var WebhookParseError = class extends FirefliesError {
99
+ code = "WEBHOOK_PARSE_FAILED";
100
+ constructor(message) {
101
+ super(message, { status: 400 });
102
+ this.name = "WebhookParseError";
103
+ }
104
+ };
105
+ var ChunkTimeoutError = class extends RealtimeError {
106
+ code = "CHUNK_TIMEOUT";
107
+ timeoutMs;
108
+ constructor(timeoutMs) {
109
+ super(`No chunks received for ${timeoutMs}ms`);
110
+ this.name = "ChunkTimeoutError";
111
+ this.timeoutMs = timeoutMs;
112
+ }
113
+ };
114
+ function parseErrorResponse(status, body, defaultMessage) {
115
+ const message = extractErrorMessage(body) ?? defaultMessage;
116
+ switch (status) {
117
+ case 401:
118
+ return new AuthenticationError(message);
119
+ case 404:
120
+ return new NotFoundError(message);
121
+ case 429: {
122
+ const retryAfter = extractRetryAfter(body);
123
+ return new RateLimitError(message, retryAfter);
124
+ }
125
+ case 400:
126
+ return new ValidationError(message);
127
+ default:
128
+ return new FirefliesError(message, { status });
129
+ }
130
+ }
131
+ function extractErrorMessage(body) {
132
+ if (typeof body === "object" && body !== null) {
133
+ const obj = body;
134
+ if (typeof obj.message === "string") {
135
+ return obj.message;
136
+ }
137
+ if (typeof obj.error === "string") {
138
+ return obj.error;
139
+ }
140
+ }
141
+ return void 0;
142
+ }
143
+ function extractRetryAfter(body) {
144
+ if (typeof body === "object" && body !== null) {
145
+ const obj = body;
146
+ if (typeof obj.retryAfter === "number") {
147
+ return obj.retryAfter;
148
+ }
149
+ }
150
+ return void 0;
151
+ }
152
+
153
+ // src/utils/rate-limit-tracker.ts
154
+ var RATE_LIMIT_REMAINING_HEADER = "x-ratelimit-remaining-api";
155
+ var RATE_LIMIT_LIMIT_HEADER = "x-ratelimit-limit-api";
156
+ var RATE_LIMIT_RESET_HEADER = "x-ratelimit-reset-api";
157
+ var RateLimitTracker = class {
158
+ _remaining;
159
+ _limit;
160
+ _resetInSeconds;
161
+ _updatedAt;
162
+ warningThreshold;
163
+ /**
164
+ * Create a new RateLimitTracker.
165
+ * @param warningThreshold - Threshold below which isLow returns true
166
+ */
167
+ constructor(warningThreshold = 10) {
168
+ this._remaining = void 0;
169
+ this._limit = void 0;
170
+ this._resetInSeconds = void 0;
171
+ this._updatedAt = 0;
172
+ this.warningThreshold = warningThreshold;
173
+ }
174
+ /**
175
+ * Get the current rate limit state.
176
+ */
177
+ get state() {
178
+ return {
179
+ remaining: this._remaining,
180
+ limit: this._limit,
181
+ resetInSeconds: this._resetInSeconds,
182
+ updatedAt: this._updatedAt
183
+ };
184
+ }
185
+ /**
186
+ * Check if remaining requests are below the warning threshold.
187
+ * Returns false if remaining is undefined (header not received).
188
+ */
189
+ get isLow() {
190
+ return this._remaining !== void 0 && this._remaining < this.warningThreshold;
191
+ }
192
+ /**
193
+ * Update state from response headers.
194
+ * Extracts x-ratelimit-remaining-api, x-ratelimit-limit-api, and x-ratelimit-reset-api headers.
195
+ *
196
+ * @param headers - Response headers (Headers object or plain object)
197
+ */
198
+ update(headers) {
199
+ const remaining = this.getHeader(headers, RATE_LIMIT_REMAINING_HEADER);
200
+ if (remaining !== null) {
201
+ const parsed = Number.parseInt(remaining, 10);
202
+ if (!Number.isNaN(parsed) && parsed >= 0) {
203
+ this._remaining = parsed;
204
+ }
205
+ }
206
+ const limit = this.getHeader(headers, RATE_LIMIT_LIMIT_HEADER);
207
+ if (limit !== null) {
208
+ const parsed = Number.parseInt(limit, 10);
209
+ if (!Number.isNaN(parsed) && parsed >= 0) {
210
+ this._limit = parsed;
211
+ }
212
+ }
213
+ const reset = this.getHeader(headers, RATE_LIMIT_RESET_HEADER);
214
+ if (reset !== null) {
215
+ const parsed = Number.parseInt(reset, 10);
216
+ if (!Number.isNaN(parsed) && parsed >= 0) {
217
+ this._resetInSeconds = parsed;
218
+ }
219
+ }
220
+ this._updatedAt = Date.now();
221
+ }
222
+ /**
223
+ * Reset the tracker to initial state.
224
+ */
225
+ reset() {
226
+ this._remaining = void 0;
227
+ this._limit = void 0;
228
+ this._resetInSeconds = void 0;
229
+ this._updatedAt = 0;
230
+ }
231
+ /**
232
+ * Calculate the delay to apply before the next request.
233
+ * Returns 0 if throttling is disabled or not needed.
234
+ *
235
+ * Uses linear interpolation: more delay as remaining approaches 0.
236
+ * - remaining >= startThreshold: no delay
237
+ * - remaining = 0: maxDelay
238
+ * - remaining in between: proportional delay
239
+ *
240
+ * @param config - Throttle configuration
241
+ * @returns Delay in milliseconds
242
+ */
243
+ getThrottleDelay(config) {
244
+ if (!config?.enabled) {
245
+ return 0;
246
+ }
247
+ if (this._remaining === void 0) {
248
+ return 0;
249
+ }
250
+ const startThreshold = Math.max(1, config.startThreshold ?? 20);
251
+ let minDelay = config.minDelay ?? 100;
252
+ let maxDelay = config.maxDelay ?? 2e3;
253
+ if (minDelay > maxDelay) {
254
+ [minDelay, maxDelay] = [maxDelay, minDelay];
255
+ }
256
+ if (this._remaining >= startThreshold) {
257
+ return 0;
258
+ }
259
+ if (this._remaining <= 0) {
260
+ return maxDelay;
261
+ }
262
+ const ratio = 1 - this._remaining / startThreshold;
263
+ return Math.round(minDelay + ratio * (maxDelay - minDelay));
264
+ }
265
+ /**
266
+ * Extract a header value from Headers object or plain object.
267
+ * For plain objects, performs case-insensitive key lookup.
268
+ */
269
+ getHeader(headers, name) {
270
+ if (headers instanceof Headers) {
271
+ return headers.get(name);
272
+ }
273
+ const lowerName = name.toLowerCase();
274
+ for (const key of Object.keys(headers)) {
275
+ if (key.toLowerCase() === lowerName) {
276
+ return headers[key] ?? null;
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+ };
282
+
283
+ // src/utils/retry.ts
284
+ var DEFAULT_OPTIONS = {
285
+ maxRetries: 3,
286
+ baseDelay: 1e3,
287
+ maxDelay: 3e4
288
+ };
289
+ async function retry(fn, options) {
290
+ const maxRetries = options?.maxRetries ?? DEFAULT_OPTIONS.maxRetries;
291
+ const baseDelay = options?.baseDelay ?? DEFAULT_OPTIONS.baseDelay;
292
+ const maxDelay = options?.maxDelay ?? DEFAULT_OPTIONS.maxDelay;
293
+ const shouldRetry = options?.shouldRetry ?? isRetryableError;
294
+ let lastError;
295
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
296
+ try {
297
+ return await fn();
298
+ } catch (error) {
299
+ lastError = error;
300
+ if (attempt >= maxRetries || !shouldRetry(error, attempt)) {
301
+ throw error;
302
+ }
303
+ const delay4 = calculateDelay(error, attempt, baseDelay, maxDelay);
304
+ await sleep(delay4);
305
+ }
306
+ }
307
+ throw lastError;
308
+ }
309
+ function isRetryableError(error) {
310
+ if (error instanceof RateLimitError) {
311
+ return true;
312
+ }
313
+ if (error instanceof TimeoutError) {
314
+ return true;
315
+ }
316
+ if (error instanceof NetworkError) {
317
+ return true;
318
+ }
319
+ if (error instanceof Error && "status" in error && typeof error.status === "number") {
320
+ return error.status >= 500 && error.status < 600;
321
+ }
322
+ return false;
323
+ }
324
+ function calculateDelay(error, attempt, baseDelay, maxDelay) {
325
+ if (error instanceof RateLimitError && error.retryAfter !== void 0) {
326
+ return Math.min(error.retryAfter, maxDelay);
327
+ }
328
+ const exponentialDelay = baseDelay * 2 ** attempt;
329
+ const jitter = Math.random() * 0.1 * exponentialDelay;
330
+ return Math.min(exponentialDelay + jitter, maxDelay);
331
+ }
332
+ function sleep(ms) {
333
+ return new Promise((resolve) => setTimeout(resolve, ms));
334
+ }
335
+
336
+ // src/graphql/client.ts
337
+ var DEFAULT_BASE_URL = "https://api.fireflies.ai/graphql";
338
+ var DEFAULT_TIMEOUT = 3e4;
339
+ var GraphQLClient = class {
340
+ apiKey;
341
+ baseUrl;
342
+ timeout;
343
+ retryOptions;
344
+ rateLimitTracker;
345
+ rateLimitConfig;
346
+ lastWarningRemaining;
347
+ constructor(config) {
348
+ if (!config.apiKey) {
349
+ throw new FirefliesError("API key is required");
350
+ }
351
+ this.apiKey = config.apiKey;
352
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
353
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
354
+ this.retryOptions = buildRetryOptions(config.retry);
355
+ if (config.rateLimit) {
356
+ const warningThreshold = config.rateLimit.warningThreshold ?? 10;
357
+ this.rateLimitTracker = new RateLimitTracker(warningThreshold);
358
+ this.rateLimitConfig = config.rateLimit;
359
+ } else {
360
+ this.rateLimitTracker = null;
361
+ this.rateLimitConfig = null;
362
+ }
363
+ }
364
+ /**
365
+ * Get the current rate limit state.
366
+ * Returns undefined if rate limit tracking is not configured.
367
+ */
368
+ get rateLimitState() {
369
+ return this.rateLimitTracker?.state;
370
+ }
371
+ /**
372
+ * Execute a GraphQL query or mutation.
373
+ *
374
+ * @param query - GraphQL query string
375
+ * @param variables - Optional query variables
376
+ * @returns The data from the GraphQL response
377
+ * @throws GraphQLError if the response contains errors
378
+ * @throws AuthenticationError if the API key is invalid
379
+ * @throws RateLimitError if rate limits are exceeded
380
+ */
381
+ async execute(query, variables) {
382
+ return retry(() => this.executeOnce(query, variables), this.retryOptions);
383
+ }
384
+ async executeOnce(query, variables) {
385
+ await this.applyThrottleDelay();
386
+ const controller = new AbortController();
387
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
388
+ try {
389
+ const response = await fetch(this.baseUrl, {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ Authorization: `Bearer ${this.apiKey}`
394
+ },
395
+ body: JSON.stringify({ query, variables }),
396
+ signal: controller.signal
397
+ });
398
+ clearTimeout(timeoutId);
399
+ this.updateRateLimitState(response.headers);
400
+ if (!response.ok) {
401
+ const body = await this.safeParseJson(response);
402
+ if (response.status === 429) {
403
+ const retryAfter = this.parseRetryAfter(response.headers);
404
+ this.invokeRateLimitedCallback(retryAfter);
405
+ throw parseErrorResponse(
406
+ response.status,
407
+ body,
408
+ `GraphQL request failed with status ${response.status}`
409
+ );
410
+ }
411
+ throw parseErrorResponse(
412
+ response.status,
413
+ body,
414
+ `GraphQL request failed with status ${response.status}`
415
+ );
416
+ }
417
+ const json = await response.json();
418
+ if (json.errors && json.errors.length > 0) {
419
+ throw this.parseGraphQLErrors(json.errors);
420
+ }
421
+ if (json.data === void 0) {
422
+ throw new FirefliesError("GraphQL response missing data field");
423
+ }
424
+ return json.data;
425
+ } catch (error) {
426
+ clearTimeout(timeoutId);
427
+ if (error instanceof FirefliesError) {
428
+ throw error;
429
+ }
430
+ if (error instanceof Error) {
431
+ if (error.name === "AbortError") {
432
+ throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
433
+ }
434
+ throw new NetworkError(`Network request failed: ${error.message}`, error);
435
+ }
436
+ throw new NetworkError("Unknown network error occurred", error);
437
+ }
438
+ }
439
+ async safeParseJson(response) {
440
+ try {
441
+ return await response.json();
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ /**
447
+ * Apply throttle delay before request if configured.
448
+ */
449
+ async applyThrottleDelay() {
450
+ if (!this.rateLimitTracker || !this.rateLimitConfig?.throttle) {
451
+ return;
452
+ }
453
+ const delay4 = this.rateLimitTracker.getThrottleDelay(this.rateLimitConfig.throttle);
454
+ if (delay4 > 0) {
455
+ await sleep2(delay4);
456
+ }
457
+ }
458
+ /**
459
+ * Update rate limit state from response headers and invoke callbacks.
460
+ */
461
+ updateRateLimitState(headers) {
462
+ if (!this.rateLimitTracker || !this.rateLimitConfig) {
463
+ return;
464
+ }
465
+ const wasLow = this.rateLimitTracker.isLow;
466
+ this.rateLimitTracker.update(headers);
467
+ const state = this.rateLimitTracker.state;
468
+ this.safeCallback(() => this.rateLimitConfig?.onUpdate?.(state));
469
+ if (this.rateLimitTracker.isLow) {
470
+ const shouldWarn = !wasLow || // Just crossed threshold
471
+ state.remaining !== void 0 && this.lastWarningRemaining !== void 0 && state.remaining < this.lastWarningRemaining;
472
+ if (shouldWarn) {
473
+ this.lastWarningRemaining = state.remaining;
474
+ this.safeCallback(() => this.rateLimitConfig?.onWarning?.(state));
475
+ }
476
+ }
477
+ }
478
+ /**
479
+ * Parse Retry-After header value.
480
+ */
481
+ parseRetryAfter(headers) {
482
+ const value = headers.get("retry-after");
483
+ if (!value) return void 0;
484
+ const parsed = Number.parseInt(value, 10);
485
+ return Number.isNaN(parsed) ? void 0 : parsed;
486
+ }
487
+ /**
488
+ * Invoke the onRateLimited callback.
489
+ */
490
+ invokeRateLimitedCallback(retryAfter) {
491
+ if (!this.rateLimitTracker || !this.rateLimitConfig?.onRateLimited) {
492
+ return;
493
+ }
494
+ const state = this.rateLimitTracker.state;
495
+ this.safeCallback(() => this.rateLimitConfig?.onRateLimited?.(state, retryAfter));
496
+ }
497
+ /**
498
+ * Safely invoke a callback, catching any errors to prevent user code from breaking the SDK.
499
+ */
500
+ safeCallback(fn) {
501
+ try {
502
+ fn();
503
+ } catch {
504
+ }
505
+ }
506
+ parseGraphQLErrors(errors) {
507
+ const firstError = errors[0];
508
+ if (!firstError) {
509
+ return new GraphQLError("Unknown GraphQL error", errors);
510
+ }
511
+ const message = firstError.message;
512
+ if (message.toLowerCase().includes("unauthorized") || message.toLowerCase().includes("authentication")) {
513
+ return parseErrorResponse(401, { message }, message);
514
+ }
515
+ if (message.toLowerCase().includes("not found")) {
516
+ return parseErrorResponse(404, { message }, message);
517
+ }
518
+ return new GraphQLError(message, errors);
519
+ }
520
+ };
521
+ function buildRetryOptions(config) {
522
+ if (!config) {
523
+ return {};
524
+ }
525
+ return {
526
+ maxRetries: config.maxRetries,
527
+ baseDelay: config.baseDelay,
528
+ maxDelay: config.maxDelay
529
+ };
530
+ }
531
+ function sleep2(ms) {
532
+ return new Promise((resolve) => setTimeout(resolve, ms));
533
+ }
534
+
535
+ // src/graphql/mutations/audio.ts
536
+ function createAudioAPI(client) {
537
+ return {
538
+ async upload(params) {
539
+ const mutation = `
540
+ mutation UploadAudio($input: AudioUploadInput!) {
541
+ uploadAudio(input: $input) {
542
+ success
543
+ title
544
+ message
545
+ }
546
+ }
547
+ `;
548
+ const data = await client.execute(mutation, {
549
+ input: params
550
+ });
551
+ return data.uploadAudio;
552
+ }
553
+ };
554
+ }
555
+
556
+ // src/graphql/mutations/transcripts.ts
557
+ function createTranscriptsMutationsAPI(client) {
558
+ return {
559
+ async delete(id) {
560
+ const mutation = `
561
+ mutation deleteTranscript($id: String!) {
562
+ deleteTranscript(id: $id) {
563
+ id
564
+ title
565
+ organizer_email
566
+ date
567
+ duration
568
+ }
569
+ }
570
+ `;
571
+ const data = await client.execute(mutation, { id });
572
+ return data.deleteTranscript;
573
+ }
574
+ };
575
+ }
576
+
577
+ // src/graphql/mutations/users.ts
578
+ function createUsersMutationsAPI(client) {
579
+ return {
580
+ async setRole(userId, role) {
581
+ const mutation = `
582
+ mutation setUserRole($userId: String!, $role: Role!) {
583
+ setUserRole(user_id: $userId, role: $role) {
584
+ id
585
+ name
586
+ email
587
+ role
588
+ }
589
+ }
590
+ `;
591
+ const data = await client.execute(mutation, {
592
+ userId,
593
+ role
594
+ });
595
+ return data.setUserRole;
596
+ }
597
+ };
598
+ }
599
+
600
+ // src/helpers/pagination.ts
601
+ async function* paginate(fetcher, pageSize = 50) {
602
+ let skip = 0;
603
+ let hasMore = true;
604
+ while (hasMore) {
605
+ const page = await fetcher(skip, pageSize);
606
+ for (const item of page) {
607
+ yield item;
608
+ }
609
+ if (page.length < pageSize) {
610
+ hasMore = false;
611
+ } else {
612
+ skip += pageSize;
613
+ }
614
+ }
615
+ }
616
+ async function collectAll(iterable) {
617
+ const items = [];
618
+ for await (const item of iterable) {
619
+ items.push(item);
620
+ }
621
+ return items;
622
+ }
623
+
624
+ // src/graphql/queries/ai-apps.ts
625
+ var AI_APP_OUTPUT_FIELDS = `
626
+ transcript_id
627
+ user_id
628
+ app_id
629
+ created_at
630
+ title
631
+ prompt
632
+ response
633
+ `;
634
+ function createAIAppsAPI(client) {
635
+ return {
636
+ async list(params) {
637
+ const query = `
638
+ query GetAIAppsOutputs(
639
+ $appId: String
640
+ $transcriptId: String
641
+ $skip: Float
642
+ $limit: Float
643
+ ) {
644
+ apps(
645
+ app_id: $appId
646
+ transcript_id: $transcriptId
647
+ skip: $skip
648
+ limit: $limit
649
+ ) {
650
+ outputs { ${AI_APP_OUTPUT_FIELDS} }
651
+ }
652
+ }
653
+ `;
654
+ const data = await client.execute(query, {
655
+ appId: params?.app_id,
656
+ transcriptId: params?.transcript_id,
657
+ skip: params?.skip,
658
+ limit: params?.limit ?? 10
659
+ });
660
+ return data.apps.outputs;
661
+ },
662
+ listAll(params) {
663
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 10);
664
+ }
665
+ };
666
+ }
667
+
668
+ // src/graphql/queries/bites.ts
669
+ var BITE_FIELDS = `
670
+ id
671
+ transcript_id
672
+ user_id
673
+ name
674
+ status
675
+ summary
676
+ summary_status
677
+ media_type
678
+ start_time
679
+ end_time
680
+ created_at
681
+ thumbnail
682
+ preview
683
+ captions {
684
+ index
685
+ text
686
+ start_time
687
+ end_time
688
+ speaker_id
689
+ speaker_name
690
+ }
691
+ sources {
692
+ src
693
+ type
694
+ }
695
+ user {
696
+ id
697
+ name
698
+ first_name
699
+ last_name
700
+ picture
701
+ }
702
+ created_from {
703
+ id
704
+ name
705
+ type
706
+ description
707
+ duration
708
+ }
709
+ privacies
710
+ `;
711
+ function createBitesAPI(client) {
712
+ return {
713
+ async get(id) {
714
+ const query = `
715
+ query Bite($biteId: ID!) {
716
+ bite(id: $biteId) { ${BITE_FIELDS} }
717
+ }
718
+ `;
719
+ const data = await client.execute(query, { biteId: id });
720
+ return data.bite;
721
+ },
722
+ async list(params) {
723
+ const query = `
724
+ query Bites(
725
+ $transcriptId: ID
726
+ $mine: Boolean
727
+ $myTeam: Boolean
728
+ $limit: Int
729
+ $skip: Int
730
+ ) {
731
+ bites(
732
+ transcript_id: $transcriptId
733
+ mine: $mine
734
+ my_team: $myTeam
735
+ limit: $limit
736
+ skip: $skip
737
+ ) { ${BITE_FIELDS} }
738
+ }
739
+ `;
740
+ const data = await client.execute(query, {
741
+ transcriptId: params.transcript_id,
742
+ mine: params.mine,
743
+ myTeam: params.my_team,
744
+ limit: params.limit ?? 50,
745
+ skip: params.skip
746
+ });
747
+ return data.bites;
748
+ },
749
+ listAll(params) {
750
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 50);
751
+ },
752
+ async create(params) {
753
+ const mutation = `
754
+ mutation CreateBite(
755
+ $transcriptId: ID!
756
+ $startTime: Float!
757
+ $endTime: Float!
758
+ $name: String
759
+ $mediaType: String
760
+ $summary: String
761
+ $privacies: [BitePrivacy!]
762
+ ) {
763
+ createBite(
764
+ transcript_Id: $transcriptId
765
+ start_time: $startTime
766
+ end_time: $endTime
767
+ name: $name
768
+ media_type: $mediaType
769
+ summary: $summary
770
+ privacies: $privacies
771
+ ) {
772
+ id
773
+ name
774
+ status
775
+ summary
776
+ }
777
+ }
778
+ `;
779
+ const data = await client.execute(mutation, {
780
+ transcriptId: params.transcript_id,
781
+ startTime: params.start_time,
782
+ endTime: params.end_time,
783
+ name: params.name,
784
+ mediaType: params.media_type,
785
+ summary: params.summary,
786
+ privacies: params.privacies
787
+ });
788
+ return data.createBite;
789
+ }
790
+ };
791
+ }
792
+
793
+ // src/graphql/queries/meetings.ts
794
+ var ACTIVE_MEETING_FIELDS = `
795
+ id
796
+ title
797
+ organizer_email
798
+ meeting_link
799
+ start_time
800
+ end_time
801
+ privacy
802
+ state
803
+ `;
804
+ function createMeetingsAPI(client) {
805
+ return {
806
+ async active(params) {
807
+ const query = `
808
+ query ActiveMeetings($email: String, $states: [MeetingState!]) {
809
+ active_meetings(input: { email: $email, states: $states }) {
810
+ ${ACTIVE_MEETING_FIELDS}
811
+ }
812
+ }
813
+ `;
814
+ const data = await client.execute(query, {
815
+ email: params?.email,
816
+ states: params?.states
817
+ });
818
+ return data.active_meetings;
819
+ },
820
+ async addBot(params) {
821
+ const mutation = `
822
+ mutation AddToLiveMeeting(
823
+ $meetingLink: String!
824
+ $title: String
825
+ $meetingPassword: String
826
+ $duration: Int
827
+ $language: String
828
+ ) {
829
+ addToLiveMeeting(
830
+ meeting_link: $meetingLink
831
+ title: $title
832
+ meeting_password: $meetingPassword
833
+ duration: $duration
834
+ language: $language
835
+ ) {
836
+ success
837
+ }
838
+ }
839
+ `;
840
+ const data = await client.execute(mutation, {
841
+ meetingLink: params.meeting_link,
842
+ title: params.title,
843
+ meetingPassword: params.password,
844
+ duration: params.duration,
845
+ language: params.language
846
+ });
847
+ return data.addToLiveMeeting;
848
+ }
849
+ };
850
+ }
851
+
852
+ // src/helpers/action-items.ts
853
+ var ASSIGNEE_PATTERNS = [
854
+ { pattern: /@(\w+)/i, group: 1 },
855
+ // @Alice
856
+ { pattern: /^(\w+):/i, group: 1 },
857
+ // Alice: at start
858
+ { pattern: /assigned to (\w+)/i, group: 1 },
859
+ // assigned to Alice
860
+ { pattern: /(\w+) will\b/i, group: 1 },
861
+ // Alice will
862
+ { pattern: /(\w+) to\b/i, group: 1 },
863
+ // Alice to (do something)
864
+ { pattern: /\s-\s*(\w+)$/i, group: 1 }
865
+ // ... - Alice
866
+ ];
867
+ var DUE_DATE_PATTERNS = [
868
+ { pattern: /by (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i, group: 1 },
869
+ { pattern: /by (tomorrow|today)/i, group: 1 },
870
+ { pattern: /by (EOD|end of day)/i, group: 1 },
871
+ { pattern: /by (EOW|end of week)/i, group: 1 },
872
+ { pattern: /due (\d{4}-\d{2}-\d{2})/i, group: 1 },
873
+ { pattern: /due (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+/i, group: 0 },
874
+ { pattern: /by (\d{1,2}\/\d{1,2})/i, group: 1 }
875
+ ];
876
+ var LIST_PREFIX_PATTERN = /^(?:[-•*]|\d+\.)\s*/;
877
+ var SECTION_HEADER_PATTERN = /^\*\*(.+)\*\*$/;
878
+ function extractActionItems(transcript, options = {}) {
879
+ const actionItemsText = transcript.summary?.action_items;
880
+ if (!actionItemsText || actionItemsText.trim().length === 0) {
881
+ return emptyResult();
882
+ }
883
+ const config = {
884
+ detectAssignees: options.detectAssignees ?? true,
885
+ detectDueDates: options.detectDueDates ?? true,
886
+ includeSourceSentences: options.includeSourceSentences ?? false,
887
+ participantNames: options.participantNames ?? []
888
+ };
889
+ const taskSentences = config.includeSourceSentences ? buildTaskSentenceLookup(transcript) : [];
890
+ const lines = actionItemsText.split(/\n/);
891
+ return parseAllLines(lines, config, taskSentences);
892
+ }
893
+ function parseAllLines(lines, config, taskSentences) {
894
+ const items = [];
895
+ const assigneeSet = /* @__PURE__ */ new Set();
896
+ let currentSectionAssignee;
897
+ for (let i = 0; i < lines.length; i++) {
898
+ const result = processLine(lines[i], i + 1, config, taskSentences, currentSectionAssignee);
899
+ if (result.type === "header") {
900
+ currentSectionAssignee = result.assignee;
901
+ } else if (result.type === "item" && result.item) {
902
+ items.push(result.item);
903
+ if (result.item.assignee) {
904
+ assigneeSet.add(result.item.assignee);
905
+ }
906
+ }
907
+ }
908
+ return buildResult(items, assigneeSet);
909
+ }
910
+ function processLine(line, lineNumber, config, taskSentences, sectionAssignee) {
911
+ if (!line) return { type: "skip" };
912
+ const trimmed = line.trim();
913
+ if (trimmed.length === 0) return { type: "skip" };
914
+ const headerMatch = trimmed.match(SECTION_HEADER_PATTERN);
915
+ if (headerMatch?.[1]) {
916
+ const headerName = headerMatch[1];
917
+ const assignee = headerName.toLowerCase() === "unassigned" ? void 0 : headerName;
918
+ return { type: "header", assignee };
919
+ }
920
+ const item = parseLine(line, lineNumber, config, taskSentences, sectionAssignee);
921
+ if (item) {
922
+ return { type: "item", item };
923
+ }
924
+ return { type: "skip" };
925
+ }
926
+ function parseLine(line, lineNumber, config, taskSentences, sectionAssignee) {
927
+ if (!line) return null;
928
+ const trimmed = line.trim();
929
+ if (trimmed.length === 0) return null;
930
+ const text = trimmed.replace(LIST_PREFIX_PATTERN, "");
931
+ if (text.length === 0) return null;
932
+ const inlineAssignee = config.detectAssignees ? detectAssignee(text, config.participantNames) : void 0;
933
+ const assignee = inlineAssignee ?? sectionAssignee;
934
+ const dueDate = config.detectDueDates ? detectDueDate(text) : void 0;
935
+ const sourceSentence = config.includeSourceSentences ? findSourceSentence(text, taskSentences) : void 0;
936
+ return { text, assignee, dueDate, lineNumber, sourceSentence };
937
+ }
938
+ function buildResult(items, assigneeSet) {
939
+ return {
940
+ items,
941
+ totalItems: items.length,
942
+ assignedItems: items.filter((i) => i.assignee !== void 0).length,
943
+ datedItems: items.filter((i) => i.dueDate !== void 0).length,
944
+ assignees: Array.from(assigneeSet)
945
+ };
946
+ }
947
+ function emptyResult() {
948
+ return {
949
+ items: [],
950
+ totalItems: 0,
951
+ assignedItems: 0,
952
+ datedItems: 0,
953
+ assignees: []
954
+ };
955
+ }
956
+ function detectAssignee(text, participantNames) {
957
+ const participantSet = new Set(participantNames.map((n) => n.toLowerCase()));
958
+ const filterByParticipants = participantSet.size > 0;
959
+ for (const { pattern, group } of ASSIGNEE_PATTERNS) {
960
+ const match = text.match(pattern);
961
+ if (match?.[group]) {
962
+ const name = match[group];
963
+ if (filterByParticipants) {
964
+ if (participantSet.has(name.toLowerCase())) {
965
+ return name;
966
+ }
967
+ continue;
968
+ }
969
+ return name;
970
+ }
971
+ }
972
+ return void 0;
973
+ }
974
+ function detectDueDate(text) {
975
+ for (const { pattern, group } of DUE_DATE_PATTERNS) {
976
+ const match = text.match(pattern);
977
+ if (match) {
978
+ if (group === 0 && match[0]) {
979
+ const fullMatch = match[0];
980
+ const dateMatch = fullMatch.match(
981
+ /(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d+/i
982
+ );
983
+ if (dateMatch?.[0]) {
984
+ return dateMatch[0];
985
+ }
986
+ }
987
+ if (match[group]) {
988
+ return match[group];
989
+ }
990
+ }
991
+ }
992
+ return void 0;
993
+ }
994
+ function buildTaskSentenceLookup(transcript) {
995
+ const sentences = transcript.sentences ?? [];
996
+ const result = [];
997
+ for (const sentence of sentences) {
998
+ const task = sentence.ai_filters?.task;
999
+ if (task) {
1000
+ result.push({
1001
+ text: sentence.text,
1002
+ task: task.toLowerCase(),
1003
+ speakerName: sentence.speaker_name,
1004
+ startTime: Number.parseFloat(sentence.start_time)
1005
+ });
1006
+ }
1007
+ }
1008
+ return result;
1009
+ }
1010
+ function findSourceSentence(actionItemText, taskSentences) {
1011
+ const normalizedItem = actionItemText.toLowerCase();
1012
+ for (const sentence of taskSentences) {
1013
+ if (normalizedItem.includes(sentence.task) || sentence.task.includes(normalizedItem)) {
1014
+ return {
1015
+ speakerName: sentence.speakerName,
1016
+ text: sentence.text,
1017
+ startTime: sentence.startTime
1018
+ };
1019
+ }
1020
+ const itemWords = new Set(normalizedItem.split(/\s+/).filter((w) => w.length > 3));
1021
+ const taskWords = sentence.task.split(/\s+/).filter((w) => w.length > 3);
1022
+ const matchingWords = taskWords.filter((w) => itemWords.has(w));
1023
+ if (taskWords.length > 0 && matchingWords.length / taskWords.length >= 0.5) {
1024
+ return {
1025
+ speakerName: sentence.speakerName,
1026
+ text: sentence.text,
1027
+ startTime: sentence.startTime
1028
+ };
1029
+ }
1030
+ }
1031
+ return void 0;
1032
+ }
1033
+
1034
+ // src/helpers/action-items-format.ts
1035
+ function filterActionItems(items, options) {
1036
+ const { assignees, assignedOnly, datedOnly } = options;
1037
+ const normalizedAssignees = assignees?.map((a) => a.toLowerCase());
1038
+ return items.filter((item) => {
1039
+ if (normalizedAssignees && normalizedAssignees.length > 0) {
1040
+ if (!item.assignee) return false;
1041
+ if (!normalizedAssignees.includes(item.assignee.toLowerCase())) return false;
1042
+ }
1043
+ if (assignedOnly && !item.assignee) {
1044
+ return false;
1045
+ }
1046
+ if (datedOnly && !item.dueDate) {
1047
+ return false;
1048
+ }
1049
+ return true;
1050
+ });
1051
+ }
1052
+ function aggregateActionItems(transcripts, extractionOptions, filterOptions) {
1053
+ if (transcripts.length === 0) {
1054
+ return emptyAggregatedResult();
1055
+ }
1056
+ const allItems = [];
1057
+ let transcriptsWithItems = 0;
1058
+ for (const transcript of transcripts) {
1059
+ const extracted = extractActionItems(transcript, extractionOptions);
1060
+ if (extracted.items.length > 0) {
1061
+ transcriptsWithItems++;
1062
+ for (const item of extracted.items) {
1063
+ allItems.push({
1064
+ ...item,
1065
+ transcriptId: transcript.id,
1066
+ transcriptTitle: transcript.title,
1067
+ transcriptDate: transcript.dateString
1068
+ });
1069
+ }
1070
+ }
1071
+ }
1072
+ const filteredItems = filterOptions ? filterActionItems(allItems, filterOptions) : allItems;
1073
+ return buildAggregatedResult(filteredItems, transcripts.length, transcriptsWithItems);
1074
+ }
1075
+ function emptyAggregatedResult() {
1076
+ return {
1077
+ items: [],
1078
+ totalItems: 0,
1079
+ transcriptsProcessed: 0,
1080
+ transcriptsWithItems: 0,
1081
+ assignedItems: 0,
1082
+ datedItems: 0,
1083
+ assignees: [],
1084
+ dateRange: { earliest: "", latest: "" }
1085
+ };
1086
+ }
1087
+ function buildAggregatedResult(items, transcriptsProcessed, transcriptsWithItems) {
1088
+ const assigneeSet = /* @__PURE__ */ new Set();
1089
+ let assignedItems = 0;
1090
+ let datedItems = 0;
1091
+ for (const item of items) {
1092
+ if (item.assignee) {
1093
+ assigneeSet.add(item.assignee);
1094
+ assignedItems++;
1095
+ }
1096
+ if (item.dueDate) {
1097
+ datedItems++;
1098
+ }
1099
+ }
1100
+ const dates = items.map((i) => i.transcriptDate).filter(Boolean).sort();
1101
+ return {
1102
+ items,
1103
+ totalItems: items.length,
1104
+ transcriptsProcessed,
1105
+ transcriptsWithItems,
1106
+ assignedItems,
1107
+ datedItems,
1108
+ assignees: Array.from(assigneeSet),
1109
+ dateRange: {
1110
+ earliest: dates[0] ?? "",
1111
+ latest: dates[dates.length - 1] ?? ""
1112
+ }
1113
+ };
1114
+ }
1115
+ function isAggregatedResult(result) {
1116
+ return "transcriptsProcessed" in result;
1117
+ }
1118
+ function isAggregatedItem(item) {
1119
+ return "transcriptId" in item;
1120
+ }
1121
+ function escapeMarkdown(text) {
1122
+ return text.replace(/\\/g, "\\\\").replace(/\*/g, "\\*").replace(/#/g, "\\#").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/_/g, "\\_").replace(/`/g, "\\`");
1123
+ }
1124
+ function getPresetOptions(preset) {
1125
+ switch (preset) {
1126
+ case "notion":
1127
+ return {
1128
+ style: "checkbox",
1129
+ includeAssignee: true,
1130
+ includeDueDate: true
1131
+ };
1132
+ case "obsidian":
1133
+ return {
1134
+ style: "checkbox",
1135
+ includeAssignee: false,
1136
+ includeDueDate: true
1137
+ };
1138
+ case "github":
1139
+ return {
1140
+ style: "checkbox",
1141
+ includeAssignee: true,
1142
+ includeDueDate: true
1143
+ };
1144
+ default:
1145
+ return {};
1146
+ }
1147
+ }
1148
+ function formatItem(item, index, options) {
1149
+ const { style, includeAssignee, includeDueDate, includeMeetingTitle } = options;
1150
+ let prefix;
1151
+ switch (style) {
1152
+ case "bullet":
1153
+ prefix = "-";
1154
+ break;
1155
+ case "numbered":
1156
+ prefix = `${index + 1}.`;
1157
+ break;
1158
+ default:
1159
+ prefix = "- [ ]";
1160
+ break;
1161
+ }
1162
+ let text = escapeMarkdown(item.text);
1163
+ const metadata = [];
1164
+ if (includeAssignee && item.assignee) {
1165
+ metadata.push(`@${item.assignee}`);
1166
+ }
1167
+ if (includeDueDate && item.dueDate) {
1168
+ metadata.push(`due: ${item.dueDate}`);
1169
+ }
1170
+ if (includeMeetingTitle && isAggregatedItem(item)) {
1171
+ metadata.push(`*${item.transcriptTitle}*`);
1172
+ }
1173
+ if (metadata.length > 0) {
1174
+ text += ` (${metadata.join(", ")})`;
1175
+ }
1176
+ return `${prefix} ${text}`;
1177
+ }
1178
+ function groupBy(items, keyFn) {
1179
+ const groups = /* @__PURE__ */ new Map();
1180
+ for (const item of items) {
1181
+ const key = keyFn(item);
1182
+ const group = groups.get(key);
1183
+ if (group) {
1184
+ group.push(item);
1185
+ } else {
1186
+ groups.set(key, [item]);
1187
+ }
1188
+ }
1189
+ return groups;
1190
+ }
1191
+ function formatSummaryLine(result) {
1192
+ return `**Summary:** ${result.totalItems} items from ${result.transcriptsProcessed} meetings (${result.assignedItems} assigned, ${result.datedItems} with due dates)`;
1193
+ }
1194
+ function sortGroupKeys(keys) {
1195
+ return keys.sort((a, b) => {
1196
+ if (a === "Unassigned") return 1;
1197
+ if (b === "Unassigned") return -1;
1198
+ return a.localeCompare(b);
1199
+ });
1200
+ }
1201
+ function formatGroupedItems(result, groupByOption, itemOptions) {
1202
+ const lines = [];
1203
+ const keyFn = getGroupKeyFn(groupByOption);
1204
+ const groups = groupBy(result.items, keyFn);
1205
+ const sortedKeys = sortGroupKeys(Array.from(groups.keys()));
1206
+ for (const key of sortedKeys) {
1207
+ const groupItems = groups.get(key);
1208
+ if (!groupItems) continue;
1209
+ lines.push(`### ${key}`);
1210
+ lines.push("");
1211
+ groupItems.forEach((item, index) => {
1212
+ lines.push(formatItem(item, index, itemOptions));
1213
+ });
1214
+ lines.push("");
1215
+ }
1216
+ return lines;
1217
+ }
1218
+ function formatFlatItems(items, itemOptions) {
1219
+ return items.map((item, index) => formatItem(item, index, itemOptions));
1220
+ }
1221
+ function formatActionItemsMarkdown(result, options = {}) {
1222
+ if (result.items.length === 0) {
1223
+ return "";
1224
+ }
1225
+ const presetOptions = getPresetOptions(options.preset);
1226
+ const mergedOptions = { ...presetOptions, ...options };
1227
+ const {
1228
+ style = "checkbox",
1229
+ groupBy: groupByOption = "none",
1230
+ includeAssignee = false,
1231
+ includeDueDate = false,
1232
+ includeMeetingTitle = false,
1233
+ includeSummary = false
1234
+ } = mergedOptions;
1235
+ const lines = [];
1236
+ if (includeSummary && isAggregatedResult(result)) {
1237
+ lines.push(formatSummaryLine(result));
1238
+ lines.push("");
1239
+ }
1240
+ const itemOptions = { style, includeAssignee, includeDueDate, includeMeetingTitle };
1241
+ const shouldGroup = groupByOption !== "none" && isAggregatedResult(result);
1242
+ if (shouldGroup) {
1243
+ lines.push(...formatGroupedItems(result, groupByOption, itemOptions));
1244
+ } else {
1245
+ lines.push(...formatFlatItems(result.items, itemOptions));
1246
+ }
1247
+ return lines.join("\n").trim();
1248
+ }
1249
+ function getGroupKeyFn(groupBy2) {
1250
+ switch (groupBy2) {
1251
+ case "assignee":
1252
+ return (item) => item.assignee ?? "Unassigned";
1253
+ case "transcript":
1254
+ return (item) => item.transcriptTitle;
1255
+ case "date":
1256
+ return (item) => item.transcriptDate;
1257
+ }
1258
+ }
1259
+
1260
+ // src/helpers/domain-utils.ts
1261
+ function extractDomain(email) {
1262
+ const atIndex = email.indexOf("@");
1263
+ if (atIndex < 0) return "";
1264
+ const domain = email.slice(atIndex + 1).toLowerCase();
1265
+ return domain || "";
1266
+ }
1267
+ function hasExternalParticipants(participants, internalDomain) {
1268
+ const normalizedInternal = internalDomain.toLowerCase();
1269
+ return participants.some((email) => {
1270
+ const domain = extractDomain(email);
1271
+ return domain !== "" && domain !== normalizedInternal;
1272
+ });
1273
+ }
1274
+
1275
+ // src/helpers/meeting-insights.ts
1276
+ function analyzeMeetings(transcripts, options = {}) {
1277
+ const { speakers, groupBy: groupBy2, topSpeakersCount = 10, topParticipantsCount = 10 } = options;
1278
+ if (transcripts.length === 0) {
1279
+ return emptyInsights();
1280
+ }
1281
+ const totalDurationMinutes = sumDurations(transcripts);
1282
+ const averageDurationMinutes = totalDurationMinutes / transcripts.length;
1283
+ const byDayOfWeek = calculateDayOfWeekStats(transcripts);
1284
+ const byTimeGroup = groupBy2 ? calculateTimeGroupStats(transcripts, groupBy2) : void 0;
1285
+ const participantData = aggregateParticipants(transcripts);
1286
+ const totalUniqueParticipants = participantData.uniqueEmails.size;
1287
+ const averageParticipantsPerMeeting = calculateAverageParticipants(transcripts);
1288
+ const topParticipants = buildTopParticipants(participantData.stats, topParticipantsCount);
1289
+ const speakerData = aggregateSpeakers(transcripts, speakers);
1290
+ const totalUniqueSpeakers = speakerData.uniqueNames.size;
1291
+ const topSpeakers = buildTopSpeakers(speakerData.stats, topSpeakersCount);
1292
+ const { earliestMeeting, latestMeeting } = findDateRange(transcripts);
1293
+ return {
1294
+ totalMeetings: transcripts.length,
1295
+ totalDurationMinutes,
1296
+ averageDurationMinutes,
1297
+ byDayOfWeek,
1298
+ byTimeGroup,
1299
+ totalUniqueParticipants,
1300
+ averageParticipantsPerMeeting,
1301
+ topParticipants,
1302
+ totalUniqueSpeakers,
1303
+ topSpeakers,
1304
+ earliestMeeting,
1305
+ latestMeeting
1306
+ };
1307
+ }
1308
+ function emptyInsights() {
1309
+ return {
1310
+ totalMeetings: 0,
1311
+ totalDurationMinutes: 0,
1312
+ averageDurationMinutes: 0,
1313
+ byDayOfWeek: emptyDayOfWeekStats(),
1314
+ byTimeGroup: void 0,
1315
+ totalUniqueParticipants: 0,
1316
+ averageParticipantsPerMeeting: 0,
1317
+ topParticipants: [],
1318
+ totalUniqueSpeakers: 0,
1319
+ topSpeakers: [],
1320
+ earliestMeeting: "",
1321
+ latestMeeting: ""
1322
+ };
1323
+ }
1324
+ function emptyDayOfWeekStats() {
1325
+ const emptyDay = () => ({ count: 0, totalMinutes: 0 });
1326
+ return {
1327
+ monday: emptyDay(),
1328
+ tuesday: emptyDay(),
1329
+ wednesday: emptyDay(),
1330
+ thursday: emptyDay(),
1331
+ friday: emptyDay(),
1332
+ saturday: emptyDay(),
1333
+ sunday: emptyDay()
1334
+ };
1335
+ }
1336
+ function sumDurations(transcripts) {
1337
+ return transcripts.reduce((sum, t) => sum + (t.duration ?? 0), 0);
1338
+ }
1339
+ function calculateDayOfWeekStats(transcripts) {
1340
+ const stats = emptyDayOfWeekStats();
1341
+ const dayNames = [
1342
+ "sunday",
1343
+ "monday",
1344
+ "tuesday",
1345
+ "wednesday",
1346
+ "thursday",
1347
+ "friday",
1348
+ "saturday"
1349
+ ];
1350
+ for (const t of transcripts) {
1351
+ const date = parseDate(t.dateString);
1352
+ if (!date) continue;
1353
+ const dayIndex = date.getUTCDay();
1354
+ const dayName = dayNames[dayIndex];
1355
+ if (dayName) {
1356
+ stats[dayName].count++;
1357
+ stats[dayName].totalMinutes += t.duration ?? 0;
1358
+ }
1359
+ }
1360
+ return stats;
1361
+ }
1362
+ function calculateTimeGroupStats(transcripts, groupBy2) {
1363
+ const groups = /* @__PURE__ */ new Map();
1364
+ for (const t of transcripts) {
1365
+ const date = parseDate(t.dateString);
1366
+ if (!date) continue;
1367
+ const period = formatPeriod(date, groupBy2);
1368
+ const existing = groups.get(period) ?? { count: 0, totalMinutes: 0 };
1369
+ existing.count++;
1370
+ existing.totalMinutes += t.duration ?? 0;
1371
+ groups.set(period, existing);
1372
+ }
1373
+ const result = [];
1374
+ for (const [period, data] of groups) {
1375
+ result.push({
1376
+ period,
1377
+ count: data.count,
1378
+ totalMinutes: data.totalMinutes,
1379
+ averageMinutes: data.totalMinutes / data.count
1380
+ });
1381
+ }
1382
+ result.sort((a, b) => a.period.localeCompare(b.period));
1383
+ return result;
1384
+ }
1385
+ function formatPeriod(date, groupBy2) {
1386
+ const year = date.getUTCFullYear();
1387
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1388
+ const day = String(date.getUTCDate()).padStart(2, "0");
1389
+ switch (groupBy2) {
1390
+ case "day":
1391
+ return `${year}-${month}-${day}`;
1392
+ case "week":
1393
+ return getISOWeek(date);
1394
+ case "month":
1395
+ return `${year}-${month}`;
1396
+ }
1397
+ }
1398
+ function getISOWeek(date) {
1399
+ const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
1400
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
1401
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
1402
+ const weekNumber = Math.ceil(((d.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
1403
+ return `${d.getUTCFullYear()}-W${String(weekNumber).padStart(2, "0")}`;
1404
+ }
1405
+ function aggregateParticipants(transcripts) {
1406
+ const uniqueEmails = /* @__PURE__ */ new Set();
1407
+ const stats = /* @__PURE__ */ new Map();
1408
+ for (const t of transcripts) {
1409
+ const participants = t.participants ?? [];
1410
+ const seenInMeeting = /* @__PURE__ */ new Set();
1411
+ for (const email of participants) {
1412
+ const normalizedEmail = email.toLowerCase();
1413
+ uniqueEmails.add(normalizedEmail);
1414
+ if (seenInMeeting.has(normalizedEmail)) continue;
1415
+ seenInMeeting.add(normalizedEmail);
1416
+ const existing = stats.get(normalizedEmail) ?? { meetingCount: 0, totalMinutes: 0 };
1417
+ existing.meetingCount++;
1418
+ existing.totalMinutes += t.duration ?? 0;
1419
+ stats.set(normalizedEmail, existing);
1420
+ }
1421
+ }
1422
+ return { uniqueEmails, stats };
1423
+ }
1424
+ function calculateAverageParticipants(transcripts) {
1425
+ if (transcripts.length === 0) return 0;
1426
+ let totalParticipants = 0;
1427
+ for (const t of transcripts) {
1428
+ const unique = new Set((t.participants ?? []).map((p) => p.toLowerCase()));
1429
+ totalParticipants += unique.size;
1430
+ }
1431
+ return totalParticipants / transcripts.length;
1432
+ }
1433
+ function buildTopParticipants(stats, limit) {
1434
+ const result = [];
1435
+ for (const [email, data] of stats) {
1436
+ result.push({
1437
+ email,
1438
+ meetingCount: data.meetingCount,
1439
+ totalMinutes: data.totalMinutes
1440
+ });
1441
+ }
1442
+ result.sort((a, b) => b.meetingCount - a.meetingCount);
1443
+ return result.slice(0, limit);
1444
+ }
1445
+ function aggregateSpeakers(transcripts, filterSpeakers) {
1446
+ const uniqueNames = /* @__PURE__ */ new Set();
1447
+ const stats = /* @__PURE__ */ new Map();
1448
+ const filterSet = filterSpeakers ? new Set(filterSpeakers) : null;
1449
+ for (const t of transcripts) {
1450
+ const sentences = t.sentences ?? [];
1451
+ for (const sentence of sentences) {
1452
+ const speakerName = sentence.speaker_name;
1453
+ if (filterSet && !filterSet.has(speakerName)) continue;
1454
+ uniqueNames.add(speakerName);
1455
+ const existing = stats.get(speakerName) ?? {
1456
+ meetingCount: 0,
1457
+ totalTalkTimeSeconds: 0,
1458
+ meetings: /* @__PURE__ */ new Set()
1459
+ };
1460
+ const duration = parseSentenceDuration(sentence);
1461
+ existing.totalTalkTimeSeconds += duration;
1462
+ if (!existing.meetings.has(t.id)) {
1463
+ existing.meetings.add(t.id);
1464
+ existing.meetingCount++;
1465
+ }
1466
+ stats.set(speakerName, existing);
1467
+ }
1468
+ }
1469
+ return { uniqueNames, stats };
1470
+ }
1471
+ function parseSentenceDuration(sentence) {
1472
+ const start = Number.parseFloat(sentence.start_time);
1473
+ const end = Number.parseFloat(sentence.end_time);
1474
+ return Math.max(0, end - start);
1475
+ }
1476
+ function buildTopSpeakers(stats, limit) {
1477
+ const result = [];
1478
+ for (const [name, data] of stats) {
1479
+ result.push({
1480
+ name,
1481
+ meetingCount: data.meetingCount,
1482
+ totalTalkTimeSeconds: data.totalTalkTimeSeconds,
1483
+ averageTalkTimeSeconds: data.meetingCount > 0 ? data.totalTalkTimeSeconds / data.meetingCount : 0
1484
+ });
1485
+ }
1486
+ result.sort((a, b) => b.totalTalkTimeSeconds - a.totalTalkTimeSeconds);
1487
+ return result.slice(0, limit);
1488
+ }
1489
+ function findDateRange(transcripts) {
1490
+ let earliest = null;
1491
+ let latest = null;
1492
+ for (const t of transcripts) {
1493
+ const date = parseDate(t.dateString);
1494
+ if (!date) continue;
1495
+ if (!earliest || date < earliest) {
1496
+ earliest = date;
1497
+ }
1498
+ if (!latest || date > latest) {
1499
+ latest = date;
1500
+ }
1501
+ }
1502
+ return {
1503
+ earliestMeeting: earliest ? formatDateOnly(earliest) : "",
1504
+ latestMeeting: latest ? formatDateOnly(latest) : ""
1505
+ };
1506
+ }
1507
+ function parseDate(dateString) {
1508
+ if (!dateString) return null;
1509
+ const date = new Date(dateString);
1510
+ return Number.isNaN(date.getTime()) ? null : date;
1511
+ }
1512
+ function formatDateOnly(date) {
1513
+ const year = date.getUTCFullYear();
1514
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
1515
+ const day = String(date.getUTCDate()).padStart(2, "0");
1516
+ return `${year}-${month}-${day}`;
1517
+ }
1518
+
1519
+ // src/helpers/search.ts
1520
+ function escapeRegex(str) {
1521
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1522
+ }
1523
+ function matchesSpeaker(sentence, speakerSet) {
1524
+ if (!speakerSet) return true;
1525
+ return speakerSet.has(sentence.speaker_name.toLowerCase());
1526
+ }
1527
+ function matchesAIFilters(sentence, filterQuestions, filterTasks) {
1528
+ if (!filterQuestions && !filterTasks) return true;
1529
+ const hasQuestion = Boolean(sentence.ai_filters?.question);
1530
+ const hasTask = Boolean(sentence.ai_filters?.task);
1531
+ if (filterQuestions && filterTasks) {
1532
+ return hasQuestion || hasTask;
1533
+ }
1534
+ if (filterQuestions) return hasQuestion;
1535
+ if (filterTasks) return hasTask;
1536
+ return true;
1537
+ }
1538
+ function extractContext(sentences, index, contextLines) {
1539
+ const beforeStart = Math.max(0, index - contextLines);
1540
+ const afterEnd = Math.min(sentences.length, index + contextLines + 1);
1541
+ return {
1542
+ before: sentences.slice(beforeStart, index).map((s) => ({
1543
+ speakerName: s.speaker_name,
1544
+ text: s.text
1545
+ })),
1546
+ after: sentences.slice(index + 1, afterEnd).map((s) => ({
1547
+ speakerName: s.speaker_name,
1548
+ text: s.text
1549
+ }))
1550
+ };
1551
+ }
1552
+ function sentenceToMatch(sentence, transcript, context) {
1553
+ return {
1554
+ transcriptId: transcript.id,
1555
+ transcriptTitle: transcript.title,
1556
+ transcriptDate: transcript.dateString,
1557
+ transcriptUrl: transcript.transcript_url,
1558
+ sentence: {
1559
+ index: sentence.index,
1560
+ text: sentence.text,
1561
+ speakerName: sentence.speaker_name,
1562
+ startTime: Number.parseFloat(sentence.start_time),
1563
+ endTime: Number.parseFloat(sentence.end_time),
1564
+ isQuestion: Boolean(sentence.ai_filters?.question),
1565
+ isTask: Boolean(sentence.ai_filters?.task)
1566
+ },
1567
+ context
1568
+ };
1569
+ }
1570
+ function searchTranscript(transcript, options) {
1571
+ const {
1572
+ query,
1573
+ caseSensitive = false,
1574
+ speakers,
1575
+ filterQuestions = false,
1576
+ filterTasks = false,
1577
+ contextLines = 1
1578
+ } = options;
1579
+ if (!query || query.trim() === "") {
1580
+ return [];
1581
+ }
1582
+ const sentences = transcript.sentences ?? [];
1583
+ if (sentences.length === 0) {
1584
+ return [];
1585
+ }
1586
+ const escapedQuery = escapeRegex(query);
1587
+ const regex = new RegExp(escapedQuery, caseSensitive ? "" : "i");
1588
+ const speakerSet = speakers ? new Set(speakers.map((s) => s.toLowerCase())) : null;
1589
+ const matches = [];
1590
+ for (let i = 0; i < sentences.length; i++) {
1591
+ const sentence = sentences[i];
1592
+ if (!sentence) continue;
1593
+ if (!regex.test(sentence.text)) continue;
1594
+ if (!matchesSpeaker(sentence, speakerSet)) continue;
1595
+ if (!matchesAIFilters(sentence, filterQuestions, filterTasks)) continue;
1596
+ const context = extractContext(sentences, i, contextLines);
1597
+ matches.push(sentenceToMatch(sentence, transcript, context));
1598
+ }
1599
+ return matches;
1600
+ }
1601
+
1602
+ // src/graphql/queries/transcripts.ts
1603
+ var TRANSCRIPT_BASE_FIELDS = `
1604
+ id
1605
+ title
1606
+ organizer_email
1607
+ host_email
1608
+ user {
1609
+ user_id
1610
+ email
1611
+ name
1612
+ }
1613
+ speakers {
1614
+ id
1615
+ name
1616
+ }
1617
+ transcript_url
1618
+ participants
1619
+ meeting_attendees {
1620
+ displayName
1621
+ email
1622
+ phoneNumber
1623
+ name
1624
+ location
1625
+ }
1626
+ meeting_attendance {
1627
+ name
1628
+ join_time
1629
+ leave_time
1630
+ }
1631
+ fireflies_users
1632
+ workspace_users
1633
+ duration
1634
+ dateString
1635
+ date
1636
+ audio_url
1637
+ video_url
1638
+ calendar_id
1639
+ meeting_info {
1640
+ fred_joined
1641
+ silent_meeting
1642
+ summary_status
1643
+ }
1644
+ cal_id
1645
+ calendar_type
1646
+ apps_preview {
1647
+ outputs {
1648
+ transcript_id
1649
+ user_id
1650
+ app_id
1651
+ created_at
1652
+ title
1653
+ prompt
1654
+ response
1655
+ }
1656
+ }
1657
+ meeting_link
1658
+ analytics {
1659
+ sentiments {
1660
+ negative_pct
1661
+ neutral_pct
1662
+ positive_pct
1663
+ }
1664
+ }
1665
+ channels {
1666
+ id
1667
+ title
1668
+ is_private
1669
+ created_at
1670
+ updated_at
1671
+ created_by
1672
+ members {
1673
+ user_id
1674
+ email
1675
+ name
1676
+ }
1677
+ }
1678
+ `;
1679
+ var SENTENCES_FIELDS = `
1680
+ sentences {
1681
+ index
1682
+ text
1683
+ raw_text
1684
+ start_time
1685
+ end_time
1686
+ speaker_id
1687
+ speaker_name
1688
+ ai_filters {
1689
+ task
1690
+ pricing
1691
+ metric
1692
+ question
1693
+ date_and_time
1694
+ text_cleanup
1695
+ sentiment
1696
+ }
1697
+ }
1698
+ `;
1699
+ var SUMMARY_FIELDS = `
1700
+ summary {
1701
+ action_items
1702
+ keywords
1703
+ outline
1704
+ overview
1705
+ shorthand_bullet
1706
+ notes
1707
+ gist
1708
+ bullet_gist
1709
+ short_summary
1710
+ short_overview
1711
+ meeting_type
1712
+ topics_discussed
1713
+ transcript_chapters
1714
+ extended_sections {
1715
+ title
1716
+ content
1717
+ }
1718
+ }
1719
+ `;
1720
+ function buildTranscriptFields(params) {
1721
+ const includeSentences = params?.includeSentences !== false;
1722
+ const includeSummary = params?.includeSummary !== false;
1723
+ let fields = TRANSCRIPT_BASE_FIELDS;
1724
+ if (includeSentences) {
1725
+ fields += SENTENCES_FIELDS;
1726
+ }
1727
+ if (includeSummary) {
1728
+ fields += SUMMARY_FIELDS;
1729
+ }
1730
+ return fields;
1731
+ }
1732
+ var TRANSCRIPT_LIST_FIELDS = `
1733
+ id
1734
+ title
1735
+ organizer_email
1736
+ transcript_url
1737
+ participants
1738
+ duration
1739
+ dateString
1740
+ date
1741
+ video_url
1742
+ meeting_info {
1743
+ fred_joined
1744
+ silent_meeting
1745
+ summary_status
1746
+ }
1747
+ `;
1748
+ function createTranscriptsAPI(client) {
1749
+ return {
1750
+ async get(id, params) {
1751
+ const fields = buildTranscriptFields(params);
1752
+ const query = `
1753
+ query GetTranscript($id: String!) {
1754
+ transcript(id: $id) {
1755
+ ${fields}
1756
+ }
1757
+ }
1758
+ `;
1759
+ const data = await client.execute(query, { id });
1760
+ return normalizeTranscript(data.transcript);
1761
+ },
1762
+ async list(params) {
1763
+ const query = `
1764
+ query ListTranscripts(
1765
+ $keyword: String
1766
+ $scope: String
1767
+ $organizers: [String!]
1768
+ $participants: [String!]
1769
+ $user_id: String
1770
+ $mine: Boolean
1771
+ $channel_id: String
1772
+ $fromDate: DateTime
1773
+ $toDate: DateTime
1774
+ $limit: Int
1775
+ $skip: Int
1776
+ $title: String
1777
+ $host_email: String
1778
+ $organizer_email: String
1779
+ $participant_email: String
1780
+ $date: Float
1781
+ ) {
1782
+ transcripts(
1783
+ keyword: $keyword
1784
+ scope: $scope
1785
+ organizers: $organizers
1786
+ participants: $participants
1787
+ user_id: $user_id
1788
+ mine: $mine
1789
+ channel_id: $channel_id
1790
+ fromDate: $fromDate
1791
+ toDate: $toDate
1792
+ limit: $limit
1793
+ skip: $skip
1794
+ title: $title
1795
+ host_email: $host_email
1796
+ organizer_email: $organizer_email
1797
+ participant_email: $participant_email
1798
+ date: $date
1799
+ ) {
1800
+ ${TRANSCRIPT_LIST_FIELDS}
1801
+ }
1802
+ }
1803
+ `;
1804
+ const variables = buildListVariables(params);
1805
+ const data = await client.execute(query, variables);
1806
+ return data.transcripts.map(normalizeTranscript);
1807
+ },
1808
+ async getSummary(id) {
1809
+ const query = `
1810
+ query GetTranscriptSummary($id: String!) {
1811
+ transcript(id: $id) {
1812
+ summary {
1813
+ action_items
1814
+ keywords
1815
+ outline
1816
+ overview
1817
+ shorthand_bullet
1818
+ notes
1819
+ gist
1820
+ bullet_gist
1821
+ short_summary
1822
+ short_overview
1823
+ meeting_type
1824
+ topics_discussed
1825
+ transcript_chapters
1826
+ extended_sections {
1827
+ title
1828
+ content
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ `;
1834
+ const data = await client.execute(query, { id });
1835
+ return data.transcript.summary;
1836
+ },
1837
+ listAll(params) {
1838
+ return paginate((skip, limit) => this.list({ ...params, skip, limit }), 50);
1839
+ },
1840
+ async search(query, params = {}) {
1841
+ const {
1842
+ caseSensitive = false,
1843
+ scope = "sentences",
1844
+ speakers,
1845
+ filterQuestions,
1846
+ filterTasks,
1847
+ contextLines = 1,
1848
+ limit,
1849
+ ...listParams
1850
+ } = params;
1851
+ const transcripts = [];
1852
+ for await (const t of this.listAll({
1853
+ keyword: query,
1854
+ scope,
1855
+ ...listParams
1856
+ })) {
1857
+ transcripts.push(t);
1858
+ if (limit && transcripts.length >= limit) break;
1859
+ }
1860
+ const allMatches = [];
1861
+ let transcriptsWithMatches = 0;
1862
+ for (const t of transcripts) {
1863
+ const full = await this.get(t.id, { includeSentences: true });
1864
+ const matches = searchTranscript(full, {
1865
+ query,
1866
+ caseSensitive,
1867
+ speakers,
1868
+ filterQuestions,
1869
+ filterTasks,
1870
+ contextLines
1871
+ });
1872
+ if (matches.length > 0) {
1873
+ transcriptsWithMatches++;
1874
+ allMatches.push(...matches);
1875
+ }
1876
+ }
1877
+ return {
1878
+ query,
1879
+ options: params,
1880
+ totalMatches: allMatches.length,
1881
+ transcriptsSearched: transcripts.length,
1882
+ transcriptsWithMatches,
1883
+ matches: allMatches
1884
+ };
1885
+ },
1886
+ async insights(params = {}) {
1887
+ const {
1888
+ fromDate,
1889
+ toDate,
1890
+ mine,
1891
+ organizers,
1892
+ participants,
1893
+ user_id,
1894
+ channel_id,
1895
+ limit,
1896
+ external,
1897
+ speakers,
1898
+ groupBy: groupBy2,
1899
+ topSpeakersCount,
1900
+ topParticipantsCount
1901
+ } = params;
1902
+ let internalDomain;
1903
+ if (external) {
1904
+ const userQuery = "query { user { email } }";
1905
+ const userData = await client.execute(userQuery);
1906
+ internalDomain = extractDomain(userData.user.email);
1907
+ }
1908
+ const transcripts = [];
1909
+ for await (const t of this.listAll({
1910
+ fromDate,
1911
+ toDate,
1912
+ mine,
1913
+ organizers,
1914
+ participants,
1915
+ user_id,
1916
+ channel_id
1917
+ })) {
1918
+ if (internalDomain && !hasExternalParticipants(t.participants, internalDomain)) {
1919
+ continue;
1920
+ }
1921
+ const full = await this.get(t.id, { includeSentences: true, includeSummary: false });
1922
+ transcripts.push(full);
1923
+ if (limit && transcripts.length >= limit) break;
1924
+ }
1925
+ return analyzeMeetings(transcripts, {
1926
+ speakers,
1927
+ groupBy: groupBy2,
1928
+ topSpeakersCount,
1929
+ topParticipantsCount
1930
+ });
1931
+ },
1932
+ async exportActionItems(params = {}) {
1933
+ const { fromDate, toDate, mine, organizers, participants, limit, filterOptions } = params;
1934
+ const transcripts = [];
1935
+ for await (const t of this.listAll({
1936
+ fromDate,
1937
+ toDate,
1938
+ mine,
1939
+ organizers,
1940
+ participants
1941
+ })) {
1942
+ const full = await this.get(t.id, { includeSentences: false, includeSummary: true });
1943
+ transcripts.push(full);
1944
+ if (limit && transcripts.length >= limit) break;
1945
+ }
1946
+ return aggregateActionItems(transcripts, {}, filterOptions);
1947
+ }
1948
+ };
1949
+ }
1950
+ function orUndefined(value) {
1951
+ return value ?? void 0;
1952
+ }
1953
+ function orEmptyArray(value) {
1954
+ return value ?? [];
1955
+ }
1956
+ function normalizeRequiredFields(raw) {
1957
+ return {
1958
+ id: raw.id,
1959
+ title: raw.title ?? "",
1960
+ organizer_email: raw.organizer_email ?? "",
1961
+ transcript_url: raw.transcript_url ?? "",
1962
+ duration: raw.duration ?? 0,
1963
+ dateString: raw.dateString ?? "",
1964
+ date: raw.date ?? 0
1965
+ };
1966
+ }
1967
+ function normalizeArrayFields(raw) {
1968
+ return {
1969
+ speakers: orEmptyArray(raw.speakers),
1970
+ participants: orEmptyArray(raw.participants),
1971
+ meeting_attendees: orEmptyArray(raw.meeting_attendees),
1972
+ meeting_attendance: orEmptyArray(raw.meeting_attendance),
1973
+ fireflies_users: orEmptyArray(raw.fireflies_users),
1974
+ workspace_users: orEmptyArray(raw.workspace_users),
1975
+ sentences: orEmptyArray(raw.sentences),
1976
+ channels: orEmptyArray(raw.channels)
1977
+ };
1978
+ }
1979
+ function normalizeOptionalFields(raw) {
1980
+ return {
1981
+ host_email: orUndefined(raw.host_email),
1982
+ user: orUndefined(raw.user),
1983
+ audio_url: orUndefined(raw.audio_url),
1984
+ video_url: orUndefined(raw.video_url),
1985
+ calendar_id: orUndefined(raw.calendar_id),
1986
+ summary: orUndefined(raw.summary),
1987
+ meeting_info: orUndefined(raw.meeting_info),
1988
+ cal_id: orUndefined(raw.cal_id),
1989
+ calendar_type: orUndefined(raw.calendar_type),
1990
+ apps_preview: orUndefined(raw.apps_preview),
1991
+ meeting_link: orUndefined(raw.meeting_link),
1992
+ analytics: orUndefined(raw.analytics)
1993
+ };
1994
+ }
1995
+ function normalizeTranscript(raw) {
1996
+ return {
1997
+ ...normalizeRequiredFields(raw),
1998
+ ...normalizeArrayFields(raw),
1999
+ ...normalizeOptionalFields(raw)
2000
+ };
2001
+ }
2002
+ function buildListVariables(params) {
2003
+ if (!params) {
2004
+ return { limit: 50 };
2005
+ }
2006
+ return {
2007
+ keyword: params.keyword,
2008
+ scope: params.scope,
2009
+ organizers: params.organizers,
2010
+ participants: params.participants,
2011
+ user_id: params.user_id,
2012
+ mine: params.mine,
2013
+ channel_id: params.channel_id,
2014
+ fromDate: params.fromDate,
2015
+ toDate: params.toDate,
2016
+ limit: params.limit ?? 50,
2017
+ skip: params.skip,
2018
+ title: params.title,
2019
+ host_email: params.host_email,
2020
+ organizer_email: params.organizer_email,
2021
+ participant_email: params.participant_email,
2022
+ date: params.date
2023
+ };
2024
+ }
2025
+
2026
+ // src/graphql/queries/users.ts
2027
+ var USER_FIELDS = `
2028
+ user_id
2029
+ email
2030
+ name
2031
+ num_transcripts
2032
+ recent_meeting
2033
+ recent_transcript
2034
+ minutes_consumed
2035
+ is_admin
2036
+ integrations
2037
+ user_groups {
2038
+ id
2039
+ name
2040
+ handle
2041
+ members {
2042
+ user_id
2043
+ email
2044
+ }
2045
+ }
2046
+ `;
2047
+ function createUsersAPI(client) {
2048
+ return {
2049
+ async me() {
2050
+ const query = `query { user { ${USER_FIELDS} } }`;
2051
+ const data = await client.execute(query);
2052
+ return data.user;
2053
+ },
2054
+ async get(id) {
2055
+ const query = `
2056
+ query User($userId: String!) {
2057
+ user(id: $userId) { ${USER_FIELDS} }
2058
+ }
2059
+ `;
2060
+ const data = await client.execute(query, { userId: id });
2061
+ return data.user;
2062
+ },
2063
+ async list() {
2064
+ const query = `query Users { users { ${USER_FIELDS} } }`;
2065
+ const data = await client.execute(query);
2066
+ return data.users;
2067
+ }
2068
+ };
2069
+ }
2070
+ var DEFAULT_WS_URL = "wss://api.fireflies.ai";
2071
+ var DEFAULT_WS_PATH = "/ws/realtime";
2072
+ var DEFAULT_TIMEOUT2 = 2e4;
2073
+ var DEFAULT_CHUNK_TIMEOUT = 2e4;
2074
+ var DEFAULT_RECONNECT_DELAY = 5e3;
2075
+ var DEFAULT_MAX_RECONNECT_DELAY = 6e4;
2076
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
2077
+ var RealtimeConnection = class {
2078
+ socket = null;
2079
+ config;
2080
+ constructor(config) {
2081
+ this.config = {
2082
+ wsUrl: DEFAULT_WS_URL,
2083
+ wsPath: DEFAULT_WS_PATH,
2084
+ timeout: DEFAULT_TIMEOUT2,
2085
+ chunkTimeout: DEFAULT_CHUNK_TIMEOUT,
2086
+ reconnect: true,
2087
+ maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
2088
+ reconnectDelay: DEFAULT_RECONNECT_DELAY,
2089
+ maxReconnectDelay: DEFAULT_MAX_RECONNECT_DELAY,
2090
+ ...config
2091
+ };
2092
+ }
2093
+ /**
2094
+ * Establish connection and wait for auth success.
2095
+ */
2096
+ async connect() {
2097
+ if (this.socket?.connected) {
2098
+ return;
2099
+ }
2100
+ const socket = socket_ioClient.io(this.config.wsUrl, {
2101
+ path: this.config.wsPath,
2102
+ auth: {
2103
+ token: `Bearer ${this.config.apiKey}`,
2104
+ transcriptId: this.config.transcriptId
2105
+ },
2106
+ // Force WebSocket transport (proven more reliable than polling)
2107
+ transports: ["websocket"],
2108
+ reconnection: this.config.reconnect,
2109
+ reconnectionDelay: this.config.reconnectDelay,
2110
+ reconnectionDelayMax: this.config.maxReconnectDelay,
2111
+ reconnectionAttempts: this.config.maxReconnectAttempts,
2112
+ // Exponential backoff factor (default 2x matches our fireflies-whiteboard pattern)
2113
+ randomizationFactor: 0.5,
2114
+ timeout: this.config.timeout,
2115
+ autoConnect: false
2116
+ });
2117
+ this.socket = socket;
2118
+ return new Promise((resolve, reject) => {
2119
+ const timeoutId = setTimeout(() => {
2120
+ socket.disconnect();
2121
+ reject(new TimeoutError(`Realtime connection timed out after ${this.config.timeout}ms`));
2122
+ }, this.config.timeout);
2123
+ const cleanup = () => clearTimeout(timeoutId);
2124
+ socket.once("auth.success", () => {
2125
+ cleanup();
2126
+ resolve();
2127
+ });
2128
+ socket.once("auth.failed", (data) => {
2129
+ cleanup();
2130
+ socket.disconnect();
2131
+ reject(new AuthenticationError(`Realtime auth failed: ${formatData(data)}`));
2132
+ });
2133
+ socket.once("connection.error", (data) => {
2134
+ cleanup();
2135
+ socket.disconnect();
2136
+ reject(new ConnectionError(`Realtime connection error: ${formatData(data)}`));
2137
+ });
2138
+ socket.once("connect_error", (error) => {
2139
+ cleanup();
2140
+ socket.disconnect();
2141
+ const message = error.message || "Connection failed";
2142
+ if (message.includes("auth") || message.includes("401") || message.includes("unauthorized")) {
2143
+ reject(new AuthenticationError(`Realtime auth failed: ${message}`));
2144
+ } else {
2145
+ reject(
2146
+ new ConnectionError(`Realtime connection failed: ${message}`, {
2147
+ cause: error
2148
+ })
2149
+ );
2150
+ }
2151
+ });
2152
+ socket.connect();
2153
+ });
2154
+ }
2155
+ /**
2156
+ * Register a chunk handler.
2157
+ * Handles both { payload: {...} } and direct payload shapes.
2158
+ */
2159
+ onChunk(handler) {
2160
+ this.socket?.on("transcription.broadcast", (data) => {
2161
+ const chunk = "payload" in data ? data.payload : data;
2162
+ handler(chunk);
2163
+ });
2164
+ }
2165
+ /**
2166
+ * Register a disconnect handler.
2167
+ */
2168
+ onDisconnect(handler) {
2169
+ this.socket?.on("disconnect", handler);
2170
+ }
2171
+ /**
2172
+ * Register a reconnect handler.
2173
+ */
2174
+ onReconnect(handler) {
2175
+ this.socket?.io.on("reconnect", handler);
2176
+ }
2177
+ /**
2178
+ * Register a reconnect attempt handler.
2179
+ */
2180
+ onReconnectAttempt(handler) {
2181
+ this.socket?.io.on("reconnect_attempt", handler);
2182
+ }
2183
+ /**
2184
+ * Register an error handler.
2185
+ */
2186
+ onError(handler) {
2187
+ this.socket?.on("connect_error", handler);
2188
+ }
2189
+ /**
2190
+ * Disconnect and cleanup.
2191
+ */
2192
+ disconnect() {
2193
+ if (this.socket) {
2194
+ this.socket.disconnect();
2195
+ this.socket = null;
2196
+ }
2197
+ }
2198
+ get connected() {
2199
+ return this.socket?.connected ?? false;
2200
+ }
2201
+ };
2202
+ function formatData(data) {
2203
+ if (data === void 0 || data === null) {
2204
+ return String(data);
2205
+ }
2206
+ try {
2207
+ return JSON.stringify(data);
2208
+ } catch {
2209
+ return String(data);
2210
+ }
2211
+ }
2212
+
2213
+ // src/realtime/stream.ts
2214
+ var RealtimeStream = class {
2215
+ connection;
2216
+ listeners = /* @__PURE__ */ new Map();
2217
+ buffer = [];
2218
+ waiters = [];
2219
+ closed = false;
2220
+ lastChunkId = null;
2221
+ lastChunk = null;
2222
+ constructor(config) {
2223
+ this.connection = new RealtimeConnection(config);
2224
+ }
2225
+ /**
2226
+ * Connect to the realtime stream.
2227
+ * @throws AuthenticationError if authentication fails
2228
+ * @throws ConnectionError if connection fails
2229
+ * @throws TimeoutError if connection times out
2230
+ */
2231
+ async connect() {
2232
+ await this.connection.connect();
2233
+ this.setupHandlers();
2234
+ this.emit("connected");
2235
+ }
2236
+ setupHandlers() {
2237
+ this.connection.onChunk((rawChunk) => {
2238
+ const isNewChunk = this.lastChunkId !== null && rawChunk.chunk_id !== this.lastChunkId;
2239
+ if (isNewChunk && this.lastChunk) {
2240
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2241
+ this.emitChunk(finalChunk);
2242
+ }
2243
+ const chunk = { ...rawChunk, isFinal: false };
2244
+ this.lastChunkId = chunk.chunk_id;
2245
+ this.lastChunk = chunk;
2246
+ this.emit("chunk", chunk);
2247
+ });
2248
+ this.connection.onDisconnect((reason) => {
2249
+ if (this.lastChunk) {
2250
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2251
+ this.emitChunk(finalChunk);
2252
+ this.lastChunk = null;
2253
+ }
2254
+ this.emit("disconnected", reason);
2255
+ if (!this.connection.connected) {
2256
+ this.closed = true;
2257
+ for (const waiter of this.waiters) {
2258
+ waiter(null);
2259
+ }
2260
+ this.waiters = [];
2261
+ }
2262
+ });
2263
+ this.connection.onReconnectAttempt((attempt) => {
2264
+ this.emit("reconnecting", attempt);
2265
+ });
2266
+ this.connection.onReconnect(() => {
2267
+ this.emit("connected");
2268
+ });
2269
+ this.connection.onError((error) => {
2270
+ this.emit("error", error);
2271
+ });
2272
+ }
2273
+ /**
2274
+ * Register an event listener.
2275
+ * @param event - Event name
2276
+ * @param handler - Event handler
2277
+ */
2278
+ on(event, handler) {
2279
+ let handlers = this.listeners.get(event);
2280
+ if (!handlers) {
2281
+ handlers = /* @__PURE__ */ new Set();
2282
+ this.listeners.set(event, handlers);
2283
+ }
2284
+ handlers.add(handler);
2285
+ return this;
2286
+ }
2287
+ /**
2288
+ * Remove an event listener.
2289
+ * @param event - Event name
2290
+ * @param handler - Event handler to remove
2291
+ */
2292
+ off(event, handler) {
2293
+ this.listeners.get(event)?.delete(handler);
2294
+ return this;
2295
+ }
2296
+ /**
2297
+ * Emit a chunk to both event listeners and async iterator buffer.
2298
+ * Used for final chunks that should be yielded by the iterator.
2299
+ */
2300
+ emitChunk(chunk) {
2301
+ this.emit("chunk", chunk);
2302
+ if (chunk.isFinal) {
2303
+ if (this.waiters.length > 0) {
2304
+ const waiter = this.waiters.shift();
2305
+ waiter?.(chunk);
2306
+ } else {
2307
+ this.buffer.push(chunk);
2308
+ }
2309
+ }
2310
+ }
2311
+ emit(event, ...args) {
2312
+ const handlers = this.listeners.get(event);
2313
+ handlers?.forEach((handler) => {
2314
+ try {
2315
+ handler(...args);
2316
+ } catch {
2317
+ }
2318
+ });
2319
+ }
2320
+ /**
2321
+ * AsyncIterable implementation for `for await` loops.
2322
+ */
2323
+ async *[Symbol.asyncIterator]() {
2324
+ if (this.closed) {
2325
+ throw new StreamClosedError();
2326
+ }
2327
+ while (!this.closed) {
2328
+ const buffered = this.buffer.shift();
2329
+ if (buffered) {
2330
+ yield buffered;
2331
+ continue;
2332
+ }
2333
+ const chunk = await new Promise((resolve) => {
2334
+ if (this.closed) {
2335
+ resolve(null);
2336
+ return;
2337
+ }
2338
+ this.waiters.push(resolve);
2339
+ });
2340
+ if (chunk === null) {
2341
+ break;
2342
+ }
2343
+ yield chunk;
2344
+ }
2345
+ }
2346
+ /**
2347
+ * Close the stream and disconnect.
2348
+ */
2349
+ close() {
2350
+ if (this.lastChunk) {
2351
+ const finalChunk = { ...this.lastChunk, isFinal: true };
2352
+ this.emitChunk(finalChunk);
2353
+ this.lastChunk = null;
2354
+ }
2355
+ this.closed = true;
2356
+ this.connection.disconnect();
2357
+ this.buffer = [];
2358
+ this.lastChunkId = null;
2359
+ for (const waiter of this.waiters) {
2360
+ waiter(null);
2361
+ }
2362
+ this.waiters = [];
2363
+ }
2364
+ /**
2365
+ * Whether the stream is currently connected.
2366
+ */
2367
+ get connected() {
2368
+ return this.connection.connected;
2369
+ }
2370
+ };
2371
+
2372
+ // src/realtime/api.ts
2373
+ function createRealtimeAPI(apiKey, baseConfig) {
2374
+ return {
2375
+ async connect(transcriptId) {
2376
+ const stream = new RealtimeStream({
2377
+ apiKey,
2378
+ transcriptId,
2379
+ ...baseConfig
2380
+ });
2381
+ await stream.connect();
2382
+ return stream;
2383
+ },
2384
+ async *stream(transcriptId) {
2385
+ const stream = new RealtimeStream({
2386
+ apiKey,
2387
+ transcriptId,
2388
+ ...baseConfig
2389
+ });
2390
+ try {
2391
+ await stream.connect();
2392
+ yield* stream;
2393
+ } finally {
2394
+ stream.close();
2395
+ }
2396
+ }
2397
+ };
2398
+ }
2399
+
2400
+ // src/client.ts
2401
+ var FirefliesClient = class {
2402
+ graphql;
2403
+ /**
2404
+ * Transcript operations: list, get, search, delete.
2405
+ */
2406
+ transcripts;
2407
+ /**
2408
+ * User operations: me, get, list, setRole.
2409
+ */
2410
+ users;
2411
+ /**
2412
+ * Bite operations: get, list, create.
2413
+ */
2414
+ bites;
2415
+ /**
2416
+ * Meeting operations: active meetings, add bot.
2417
+ */
2418
+ meetings;
2419
+ /**
2420
+ * Audio operations: upload audio for transcription.
2421
+ */
2422
+ audio;
2423
+ /**
2424
+ * AI Apps operations: list outputs.
2425
+ */
2426
+ aiApps;
2427
+ /**
2428
+ * Realtime transcription streaming.
2429
+ */
2430
+ realtime;
2431
+ /**
2432
+ * Create a new Fireflies client.
2433
+ *
2434
+ * @param config - Client configuration
2435
+ * @throws FirefliesError if API key is missing
2436
+ */
2437
+ constructor(config) {
2438
+ this.graphql = new GraphQLClient(config);
2439
+ const transcriptsQueries = createTranscriptsAPI(this.graphql);
2440
+ const transcriptsMutations = createTranscriptsMutationsAPI(this.graphql);
2441
+ this.transcripts = { ...transcriptsQueries, ...transcriptsMutations };
2442
+ const usersQueries = createUsersAPI(this.graphql);
2443
+ const usersMutations = createUsersMutationsAPI(this.graphql);
2444
+ this.users = { ...usersQueries, ...usersMutations };
2445
+ this.bites = createBitesAPI(this.graphql);
2446
+ this.meetings = createMeetingsAPI(this.graphql);
2447
+ this.audio = createAudioAPI(this.graphql);
2448
+ this.aiApps = createAIAppsAPI(this.graphql);
2449
+ this.realtime = createRealtimeAPI(config.apiKey);
2450
+ }
2451
+ /**
2452
+ * Get the current rate limit state.
2453
+ * Returns undefined if rate limit tracking is not configured.
2454
+ *
2455
+ * @example
2456
+ * ```typescript
2457
+ * const client = new FirefliesClient({
2458
+ * apiKey: '...',
2459
+ * rateLimit: { warningThreshold: 10 }
2460
+ * });
2461
+ *
2462
+ * await client.users.me();
2463
+ * console.log(client.rateLimits);
2464
+ * // { remaining: 59, limit: 60, resetInSeconds: 60, updatedAt: 1706299500000 }
2465
+ * ```
2466
+ */
2467
+ get rateLimits() {
2468
+ return this.graphql.rateLimitState;
2469
+ }
2470
+ };
2471
+
2472
+ // src/helpers/accumulator.ts
2473
+ var TranscriptAccumulator = class {
2474
+ turns = [];
2475
+ currentTurn = null;
2476
+ seenChunkIds = /* @__PURE__ */ new Set();
2477
+ /**
2478
+ * Add a chunk to the accumulator.
2479
+ *
2480
+ * Only final chunks are accumulated; non-final chunks are ignored.
2481
+ * Duplicate chunk IDs are also ignored.
2482
+ *
2483
+ * @param chunk - The transcription chunk to add
2484
+ */
2485
+ add(chunk) {
2486
+ if (!chunk.isFinal) return;
2487
+ if (this.seenChunkIds.has(chunk.chunk_id)) return;
2488
+ this.seenChunkIds.add(chunk.chunk_id);
2489
+ if (this.currentTurn && this.currentTurn.speaker === chunk.speaker_name) {
2490
+ this.currentTurn.text += ` ${chunk.text}`;
2491
+ this.currentTurn.endTime = chunk.end_time;
2492
+ this.currentTurn.chunks.push(chunk);
2493
+ } else {
2494
+ this.currentTurn = {
2495
+ speaker: chunk.speaker_name,
2496
+ text: chunk.text,
2497
+ startTime: chunk.start_time,
2498
+ endTime: chunk.end_time,
2499
+ chunks: [chunk]
2500
+ };
2501
+ this.turns.push(this.currentTurn);
2502
+ }
2503
+ }
2504
+ /**
2505
+ * Get the current accumulated transcript state.
2506
+ *
2507
+ * Statistics are computed on demand to ensure accuracy.
2508
+ *
2509
+ * @returns The accumulated transcript with turns, speakers, and statistics
2510
+ */
2511
+ getTranscript() {
2512
+ const speakers = this.getUniqueSpeakers();
2513
+ const wordCount = this.computeWordCount();
2514
+ const duration = this.computeDuration();
2515
+ return {
2516
+ turns: this.turns,
2517
+ speakers,
2518
+ wordCount,
2519
+ duration,
2520
+ chunkCount: this.seenChunkIds.size
2521
+ };
2522
+ }
2523
+ /**
2524
+ * Clear all accumulated data.
2525
+ *
2526
+ * Useful for resetting the accumulator between sessions.
2527
+ */
2528
+ clear() {
2529
+ this.turns = [];
2530
+ this.currentTurn = null;
2531
+ this.seenChunkIds.clear();
2532
+ }
2533
+ getUniqueSpeakers() {
2534
+ const seen = /* @__PURE__ */ new Set();
2535
+ const speakers = [];
2536
+ for (const turn of this.turns) {
2537
+ if (!seen.has(turn.speaker)) {
2538
+ seen.add(turn.speaker);
2539
+ speakers.push(turn.speaker);
2540
+ }
2541
+ }
2542
+ return speakers;
2543
+ }
2544
+ computeWordCount() {
2545
+ let count = 0;
2546
+ for (const turn of this.turns) {
2547
+ if (turn.text.length === 0) continue;
2548
+ const words = turn.text.trim().split(/\s+/);
2549
+ count += words.filter((w) => w.length > 0).length;
2550
+ }
2551
+ return count;
2552
+ }
2553
+ computeDuration() {
2554
+ const firstTurn = this.turns[0];
2555
+ const lastTurn = this.turns[this.turns.length - 1];
2556
+ if (!firstTurn || !lastTurn) return 0;
2557
+ const firstChunk = firstTurn.chunks[0];
2558
+ if (!firstChunk) return 0;
2559
+ return lastTurn.endTime - firstChunk.start_time;
2560
+ }
2561
+ };
2562
+
2563
+ // src/helpers/batch.ts
2564
+ async function* batch(items, processor, options = {}) {
2565
+ const { delayMs = 100, handleRateLimit = true, maxRateLimitRetries = 3 } = options;
2566
+ let isFirst = true;
2567
+ for await (const item of items) {
2568
+ if (!isFirst && delayMs > 0) {
2569
+ await delay(delayMs);
2570
+ }
2571
+ isFirst = false;
2572
+ yield await processWithRetry(item, processor, {
2573
+ handleRateLimit,
2574
+ maxRateLimitRetries
2575
+ });
2576
+ }
2577
+ }
2578
+ async function batchAll(items, processor, options = {}) {
2579
+ const { continueOnError = false, ...batchOptions } = options;
2580
+ const results = [];
2581
+ const errors = [];
2582
+ for await (const batchResult of batch(items, processor, batchOptions)) {
2583
+ if (batchResult.error) {
2584
+ if (!continueOnError) {
2585
+ throw batchResult.error;
2586
+ }
2587
+ errors.push(batchResult.error);
2588
+ } else {
2589
+ results.push(batchResult.result);
2590
+ }
2591
+ }
2592
+ return results;
2593
+ }
2594
+ async function processWithRetry(item, processor, options) {
2595
+ const { handleRateLimit, maxRateLimitRetries } = options;
2596
+ let retries = 0;
2597
+ while (true) {
2598
+ try {
2599
+ const result = await processor(item);
2600
+ return { item, result };
2601
+ } catch (err) {
2602
+ if (handleRateLimit && err instanceof RateLimitError && retries < maxRateLimitRetries) {
2603
+ const waitTime = err.retryAfter ?? 1e3;
2604
+ await delay(waitTime);
2605
+ retries++;
2606
+ continue;
2607
+ }
2608
+ return {
2609
+ item,
2610
+ error: err instanceof Error ? err : new Error(String(err))
2611
+ };
2612
+ }
2613
+ }
2614
+ }
2615
+ function delay(ms) {
2616
+ return new Promise((resolve) => setTimeout(resolve, ms));
2617
+ }
2618
+
2619
+ // src/helpers/external-questions.ts
2620
+ function findExternalParticipantQuestions(transcript, internalDomains) {
2621
+ const domains = normalizeDomains(internalDomains);
2622
+ const speakerEmailMap = buildSpeakerEmailMap(transcript);
2623
+ const { externalSpeakers } = classifySpeakers(transcript, speakerEmailMap, domains);
2624
+ const questions = [];
2625
+ for (const sentence of transcript.sentences ?? []) {
2626
+ if (!sentence.ai_filters?.question) {
2627
+ continue;
2628
+ }
2629
+ if (!externalSpeakers.has(sentence.speaker_name)) {
2630
+ continue;
2631
+ }
2632
+ questions.push({
2633
+ text: sentence.text,
2634
+ speakerName: sentence.speaker_name,
2635
+ speakerEmail: speakerEmailMap.get(sentence.speaker_name),
2636
+ sentenceIndex: sentence.index,
2637
+ startTime: sentence.start_time,
2638
+ endTime: sentence.end_time
2639
+ });
2640
+ }
2641
+ const externalParticipants = Array.from(externalSpeakers).map((name) => ({
2642
+ name,
2643
+ email: speakerEmailMap.get(name)
2644
+ }));
2645
+ return {
2646
+ externalParticipants,
2647
+ questions,
2648
+ totalQuestions: questions.length
2649
+ };
2650
+ }
2651
+ function normalizeDomains(input) {
2652
+ const raw = Array.isArray(input) ? input : [input];
2653
+ return raw.map((d) => {
2654
+ const domain = d.toLowerCase().trim();
2655
+ return domain.startsWith("@") ? domain : `@${domain}`;
2656
+ });
2657
+ }
2658
+ function buildSpeakerEmailMap(transcript) {
2659
+ const map = /* @__PURE__ */ new Map();
2660
+ for (const attendee of transcript.meeting_attendees ?? []) {
2661
+ if (attendee.displayName && attendee.email) {
2662
+ map.set(attendee.displayName, attendee.email.toLowerCase());
2663
+ }
2664
+ if (attendee.name && attendee.email) {
2665
+ map.set(attendee.name, attendee.email.toLowerCase());
2666
+ }
2667
+ }
2668
+ return map;
2669
+ }
2670
+ function classifySpeakers(transcript, speakerEmailMap, internalDomains) {
2671
+ const internalSpeakers = /* @__PURE__ */ new Set();
2672
+ const externalSpeakers = /* @__PURE__ */ new Set();
2673
+ const speakerNames = new Set((transcript.sentences ?? []).map((s) => s.speaker_name));
2674
+ for (const name of speakerNames) {
2675
+ const email = speakerEmailMap.get(name);
2676
+ if (isInternal(email, internalDomains)) {
2677
+ internalSpeakers.add(name);
2678
+ } else {
2679
+ externalSpeakers.add(name);
2680
+ }
2681
+ }
2682
+ return { internalSpeakers, externalSpeakers };
2683
+ }
2684
+ function isInternal(email, internalDomains) {
2685
+ if (!email) {
2686
+ return false;
2687
+ }
2688
+ const lowerEmail = email.toLowerCase();
2689
+ return internalDomains.some((domain) => lowerEmail.endsWith(domain));
2690
+ }
2691
+
2692
+ // src/helpers/markdown.ts
2693
+ var DEFAULT_OPTIONS2 = {
2694
+ includeMetadata: true,
2695
+ includeSummary: true,
2696
+ includeActionItems: true,
2697
+ actionItemFormat: "checkbox",
2698
+ includeTimestamps: false,
2699
+ speakerFormat: "bold",
2700
+ groupBySpeaker: true
2701
+ };
2702
+ var DEFAULT_CHUNKS_OPTIONS = {
2703
+ title: "Live Transcript",
2704
+ includeTimestamps: false,
2705
+ speakerFormat: "bold",
2706
+ groupBySpeaker: true
2707
+ };
2708
+ async function transcriptToMarkdown(transcript, options = {}) {
2709
+ const opts = { ...DEFAULT_OPTIONS2, ...options };
2710
+ const sections = [];
2711
+ if (opts.includeMetadata) {
2712
+ sections.push(formatMetadata(transcript));
2713
+ }
2714
+ if (opts.includeSummary && transcript.summary) {
2715
+ sections.push(formatSummary(transcript.summary, opts));
2716
+ }
2717
+ if (transcript.sentences && transcript.sentences.length > 0) {
2718
+ sections.push(formatTranscript(transcript.sentences, opts));
2719
+ }
2720
+ const content = sections.join("\n\n---\n\n");
2721
+ await writeIfOutputPath(content, options.outputPath);
2722
+ return content;
2723
+ }
2724
+ async function chunksToMarkdown(chunks, options = {}) {
2725
+ const opts = { ...DEFAULT_CHUNKS_OPTIONS, ...options };
2726
+ const lines = [`# ${opts.title}`];
2727
+ if (chunks.length === 0) {
2728
+ lines.push("", "## Transcript", "", "*No transcription data*");
2729
+ } else {
2730
+ lines.push("", "## Transcript");
2731
+ if (opts.groupBySpeaker) {
2732
+ const groups = groupChunksBySpeaker(chunks);
2733
+ for (const group of groups) {
2734
+ lines.push("", formatChunkGroup(group, opts));
2735
+ }
2736
+ } else {
2737
+ for (const chunk of chunks) {
2738
+ lines.push("", formatChunk(chunk, opts));
2739
+ }
2740
+ }
2741
+ }
2742
+ const content = lines.join("\n");
2743
+ await writeIfOutputPath(content, options.outputPath);
2744
+ return content;
2745
+ }
2746
+ function formatMetadata(transcript) {
2747
+ const lines = [`# ${transcript.title || "Untitled Meeting"}`];
2748
+ if (transcript.dateString) {
2749
+ lines.push(`
2750
+ **Date:** ${formatDate(transcript.dateString)}`);
2751
+ }
2752
+ const duration = calculateDuration(transcript);
2753
+ if (duration > 0) {
2754
+ lines.push(`**Duration:** ${formatDuration(duration)}`);
2755
+ }
2756
+ const participants = getParticipantNames(transcript);
2757
+ if (participants.length > 0) {
2758
+ lines.push(`**Participants:** ${participants.join(", ")}`);
2759
+ }
2760
+ return lines.join("\n");
2761
+ }
2762
+ function calculateDuration(transcript) {
2763
+ if (transcript.sentences && transcript.sentences.length > 0) {
2764
+ const lastSentence = transcript.sentences[transcript.sentences.length - 1];
2765
+ if (lastSentence) {
2766
+ return parseFloat(lastSentence.end_time);
2767
+ }
2768
+ }
2769
+ return transcript.duration || 0;
2770
+ }
2771
+ function formatSummary(summary, opts) {
2772
+ const sections = ["## Summary"];
2773
+ if (summary.gist) {
2774
+ sections.push("", summary.gist);
2775
+ }
2776
+ if (summary.bullet_gist) {
2777
+ const bullets = parseMultilineField(summary.bullet_gist);
2778
+ if (bullets.length > 0) {
2779
+ sections.push("", "### Key Points");
2780
+ sections.push(bullets.map((p) => `- ${p}`).join("\n"));
2781
+ }
2782
+ }
2783
+ if (opts.includeActionItems && summary.action_items) {
2784
+ const items = parseMultilineField(summary.action_items);
2785
+ if (items.length > 0) {
2786
+ sections.push("", "### Action Items");
2787
+ const prefix = opts.actionItemFormat === "checkbox" ? "- [ ] " : "- ";
2788
+ sections.push(items.map((a) => `${prefix}${a}`).join("\n"));
2789
+ }
2790
+ }
2791
+ return sections.join("\n");
2792
+ }
2793
+ function formatTranscript(sentences, opts) {
2794
+ const lines = ["## Transcript"];
2795
+ if (opts.groupBySpeaker) {
2796
+ const groups = groupSentencesBySpeaker(sentences);
2797
+ for (const group of groups) {
2798
+ lines.push("", formatSpeakerGroup(group, opts));
2799
+ }
2800
+ } else {
2801
+ for (const sentence of sentences) {
2802
+ lines.push("", formatSentence(sentence, opts));
2803
+ }
2804
+ }
2805
+ return lines.join("\n");
2806
+ }
2807
+ function groupSentencesBySpeaker(sentences) {
2808
+ const groups = [];
2809
+ let current = null;
2810
+ for (const sentence of sentences) {
2811
+ if (!current || current.speakerName !== sentence.speaker_name) {
2812
+ current = { speakerName: sentence.speaker_name, sentences: [] };
2813
+ groups.push(current);
2814
+ }
2815
+ current.sentences.push(sentence);
2816
+ }
2817
+ return groups;
2818
+ }
2819
+ function formatSpeakerGroup(group, opts) {
2820
+ const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
2821
+ const text = group.sentences.map((s) => s.text).join(" ");
2822
+ const firstSentence = group.sentences[0];
2823
+ if (opts.includeTimestamps && firstSentence) {
2824
+ const timestamp = formatTimestamp(firstSentence.start_time);
2825
+ return `${timestamp} ${speaker} ${text}`;
2826
+ }
2827
+ return `${speaker} ${text}`;
2828
+ }
2829
+ function formatSentence(sentence, opts) {
2830
+ const speaker = formatSpeakerName(sentence.speaker_name, opts.speakerFormat);
2831
+ if (opts.includeTimestamps) {
2832
+ const timestamp = formatTimestamp(sentence.start_time);
2833
+ return `${timestamp} ${speaker} ${sentence.text}`;
2834
+ }
2835
+ return `${speaker} ${sentence.text}`;
2836
+ }
2837
+ function groupChunksBySpeaker(chunks) {
2838
+ const groups = [];
2839
+ let current = null;
2840
+ for (const chunk of chunks) {
2841
+ if (!current || current.speakerName !== chunk.speaker_name) {
2842
+ current = { speakerName: chunk.speaker_name, chunks: [] };
2843
+ groups.push(current);
2844
+ }
2845
+ current.chunks.push(chunk);
2846
+ }
2847
+ return groups;
2848
+ }
2849
+ function formatChunkGroup(group, opts) {
2850
+ const speaker = formatSpeakerName(group.speakerName, opts.speakerFormat);
2851
+ const text = group.chunks.map((c) => c.text).join(" ");
2852
+ const firstChunk = group.chunks[0];
2853
+ if (opts.includeTimestamps && firstChunk) {
2854
+ const timestamp = formatTimestamp(firstChunk.start_time.toString());
2855
+ return `${timestamp} ${speaker} ${text}`;
2856
+ }
2857
+ return `${speaker} ${text}`;
2858
+ }
2859
+ function formatChunk(chunk, opts) {
2860
+ const speaker = formatSpeakerName(chunk.speaker_name, opts.speakerFormat);
2861
+ if (opts.includeTimestamps) {
2862
+ const timestamp = formatTimestamp(chunk.start_time.toString());
2863
+ return `${timestamp} ${speaker} ${chunk.text}`;
2864
+ }
2865
+ return `${speaker} ${chunk.text}`;
2866
+ }
2867
+ function formatSpeakerName(name, format) {
2868
+ switch (format) {
2869
+ case "bold":
2870
+ return `**${name}:**`;
2871
+ case "plain":
2872
+ return `${name}:`;
2873
+ }
2874
+ }
2875
+ function formatTimestamp(startTime) {
2876
+ const seconds = parseFloat(startTime);
2877
+ const mins = Math.floor(seconds / 60);
2878
+ const secs = Math.floor(seconds % 60);
2879
+ return `[${mins}:${secs.toString().padStart(2, "0")}]`;
2880
+ }
2881
+ function formatDuration(seconds) {
2882
+ const hours = Math.floor(seconds / 3600);
2883
+ const mins = Math.floor(seconds % 3600 / 60);
2884
+ if (hours > 0) {
2885
+ return `${hours}h ${mins}m`;
2886
+ }
2887
+ return `${mins} minutes`;
2888
+ }
2889
+ function formatDate(isoString) {
2890
+ return new Date(isoString).toLocaleDateString("en-US", {
2891
+ weekday: "long",
2892
+ year: "numeric",
2893
+ month: "long",
2894
+ day: "numeric"
2895
+ });
2896
+ }
2897
+ function getParticipantNames(transcript) {
2898
+ if (transcript.meeting_attendees?.length) {
2899
+ return transcript.meeting_attendees.map((a) => a.displayName || a.name || a.email).filter(Boolean);
2900
+ }
2901
+ return transcript.speakers?.map((s) => s.name) || [];
2902
+ }
2903
+ function parseMultilineField(value) {
2904
+ return value.split(/\n/).map((line) => line.trim()).filter((line) => line.length > 0);
2905
+ }
2906
+ async function writeIfOutputPath(content, outputPath) {
2907
+ if (outputPath) {
2908
+ const { writeFile } = await import('fs/promises');
2909
+ await writeFile(outputPath, content, "utf-8");
2910
+ }
2911
+ }
2912
+
2913
+ // src/utils/dedup.ts
2914
+ var Deduplicator = class {
2915
+ seen = /* @__PURE__ */ new Set();
2916
+ queue = [];
2917
+ maxSize;
2918
+ constructor(maxSize = 1e3) {
2919
+ this.maxSize = maxSize;
2920
+ }
2921
+ /**
2922
+ * Check if item is a duplicate and mark as seen.
2923
+ * @param key - Unique key to check
2924
+ * @returns true if duplicate, false if new
2925
+ */
2926
+ isDuplicate(key) {
2927
+ if (this.seen.has(key)) {
2928
+ return true;
2929
+ }
2930
+ this.seen.add(key);
2931
+ this.queue.push(key);
2932
+ while (this.queue.length > this.maxSize) {
2933
+ const oldest = this.queue.shift();
2934
+ if (oldest) this.seen.delete(oldest);
2935
+ }
2936
+ return false;
2937
+ }
2938
+ /**
2939
+ * Clear all tracked keys.
2940
+ */
2941
+ clear() {
2942
+ this.seen.clear();
2943
+ this.queue = [];
2944
+ }
2945
+ /**
2946
+ * Get current number of tracked keys.
2947
+ */
2948
+ get size() {
2949
+ return this.seen.size;
2950
+ }
2951
+ };
2952
+
2953
+ // src/helpers/multi-user.ts
2954
+ async function* getMeetingsForMultipleUsers(apiKeys, options = {}) {
2955
+ const { deduplicate = true, filter, delayMs = 100 } = options;
2956
+ const dedup = deduplicate ? new Deduplicator() : null;
2957
+ let needsDelay = false;
2958
+ for (const [sourceIndex, apiKey] of apiKeys.entries()) {
2959
+ const client = new FirefliesClient({ apiKey });
2960
+ for await (const transcript of client.transcripts.listAll(filter)) {
2961
+ if (needsDelay && delayMs > 0) {
2962
+ await delay2(delayMs);
2963
+ }
2964
+ needsDelay = true;
2965
+ if (dedup?.isDuplicate(transcript.id)) {
2966
+ continue;
2967
+ }
2968
+ yield {
2969
+ transcript,
2970
+ sourceApiKey: apiKey,
2971
+ sourceIndex
2972
+ };
2973
+ }
2974
+ }
2975
+ }
2976
+ function delay2(ms) {
2977
+ return new Promise((resolve) => setTimeout(resolve, ms));
2978
+ }
2979
+
2980
+ // src/helpers/normalize.ts
2981
+ var DEFAULT_OPTIONS3 = {
2982
+ timeUnit: "seconds",
2983
+ includeRawData: false,
2984
+ includeAIFilters: true,
2985
+ includeSummary: true,
2986
+ resolveSpeakerName: (speaker) => speaker.name,
2987
+ enrichParticipant: () => ({})
2988
+ };
2989
+ function normalizeTranscript2(transcript, options) {
2990
+ const opts = { ...DEFAULT_OPTIONS3, ...options };
2991
+ const speakers = normalizeSpeakers(transcript.speakers ?? [], transcript, opts);
2992
+ const sentences = normalizeSentences(transcript.sentences ?? [], opts);
2993
+ const participants = normalizeParticipants(transcript, opts);
2994
+ const summary = opts.includeSummary ? normalizeSummary(transcript.summary) : void 0;
2995
+ const attendees = normalizeAttendees(transcript.meeting_attendance ?? []);
2996
+ const channels = normalizeChannels(transcript.channels ?? []);
2997
+ const analytics = normalizeAnalytics(transcript.analytics);
2998
+ return {
2999
+ id: `fireflies:${transcript.id}`,
3000
+ title: transcript.title,
3001
+ date: new Date(transcript.date),
3002
+ duration: transcript.duration * 60,
3003
+ // minutes → seconds
3004
+ url: transcript.transcript_url,
3005
+ speakers,
3006
+ sentences,
3007
+ participants,
3008
+ summary,
3009
+ attendees,
3010
+ channels,
3011
+ analytics,
3012
+ source: {
3013
+ provider: "fireflies",
3014
+ originalId: transcript.id,
3015
+ rawData: opts.includeRawData ? transcript : void 0
3016
+ }
3017
+ };
3018
+ }
3019
+ function createNormalizer(options) {
3020
+ return (transcript) => normalizeTranscript2(transcript, options);
3021
+ }
3022
+ function normalizeSpeakers(speakers, transcript, opts) {
3023
+ return speakers.map((speaker) => ({
3024
+ id: speaker.id,
3025
+ name: opts.resolveSpeakerName(speaker, transcript)
3026
+ }));
3027
+ }
3028
+ function normalizeSentences(sentences, opts) {
3029
+ const timeMultiplier = opts.timeUnit === "milliseconds" ? 1e3 : 1;
3030
+ return sentences.map((sentence) => {
3031
+ const normalized = {
3032
+ index: sentence.index,
3033
+ speakerId: sentence.speaker_id,
3034
+ speakerName: sentence.speaker_name,
3035
+ text: sentence.text,
3036
+ rawText: sentence.raw_text,
3037
+ startTime: parseTime(sentence.start_time) * timeMultiplier,
3038
+ endTime: parseTime(sentence.end_time) * timeMultiplier
3039
+ };
3040
+ if (opts.includeAIFilters && sentence.ai_filters) {
3041
+ const sentiment = mapSentiment(sentence.ai_filters.sentiment);
3042
+ if (sentiment) {
3043
+ normalized.sentiment = sentiment;
3044
+ }
3045
+ if (sentence.ai_filters.question) {
3046
+ normalized.isQuestion = true;
3047
+ }
3048
+ if (sentence.ai_filters.task) {
3049
+ normalized.isActionItem = true;
3050
+ }
3051
+ }
3052
+ return normalized;
3053
+ });
3054
+ }
3055
+ function parseTime(timeStr) {
3056
+ const parsed = Number.parseFloat(timeStr);
3057
+ return Number.isNaN(parsed) ? 0 : parsed;
3058
+ }
3059
+ function mapSentiment(sentiment) {
3060
+ if (!sentiment) return void 0;
3061
+ const lower = sentiment.toLowerCase();
3062
+ if (lower === "positive") return "positive";
3063
+ if (lower === "negative") return "negative";
3064
+ if (lower === "neutral") return "neutral";
3065
+ return void 0;
3066
+ }
3067
+ function normalizeParticipants(transcript, opts) {
3068
+ const participants = transcript.participants ?? [];
3069
+ const attendeeMap = /* @__PURE__ */ new Map();
3070
+ for (const attendee of transcript.meeting_attendees ?? []) {
3071
+ if (attendee.email && attendee.name) {
3072
+ attendeeMap.set(attendee.email.toLowerCase(), attendee.name);
3073
+ }
3074
+ }
3075
+ return participants.map((email) => {
3076
+ const isOrganizer = email.toLowerCase() === transcript.organizer_email.toLowerCase();
3077
+ const attendeeName = attendeeMap.get(email.toLowerCase());
3078
+ const enrichment = opts.enrichParticipant(email, transcript);
3079
+ return {
3080
+ name: enrichment.name ?? attendeeName ?? "",
3081
+ email,
3082
+ role: enrichment.role ?? (isOrganizer ? "organizer" : "attendee")
3083
+ };
3084
+ });
3085
+ }
3086
+ function normalizeSummary(summary) {
3087
+ if (!summary) return void 0;
3088
+ const keyPoints = parseKeyPoints(summary.shorthand_bullet);
3089
+ return {
3090
+ overview: summary.overview,
3091
+ keyPoints: keyPoints.length > 0 ? keyPoints : void 0,
3092
+ actionItems: summary.action_items,
3093
+ outline: summary.outline,
3094
+ topics: summary.topics_discussed
3095
+ };
3096
+ }
3097
+ function parseKeyPoints(shorthandBullet) {
3098
+ if (!shorthandBullet) return [];
3099
+ return shorthandBullet.split("\n").map((line) => line.replace(/^[-*•]\s*/, "").trim()).filter((line) => line.length > 0);
3100
+ }
3101
+ function normalizeAttendees(attendance) {
3102
+ return attendance.map((a) => ({
3103
+ name: a.name,
3104
+ joinTime: a.join_time ? new Date(a.join_time) : void 0,
3105
+ leaveTime: a.leave_time ? new Date(a.leave_time) : void 0
3106
+ }));
3107
+ }
3108
+ function normalizeChannels(channels) {
3109
+ return channels.map((ch) => ({
3110
+ id: ch.id,
3111
+ title: ch.title,
3112
+ isPrivate: ch.is_private ?? false
3113
+ }));
3114
+ }
3115
+ function normalizeAnalytics(analytics) {
3116
+ if (!analytics?.sentiments) return void 0;
3117
+ return {
3118
+ sentiments: {
3119
+ positive: analytics.sentiments.positive_pct ?? 0,
3120
+ neutral: analytics.sentiments.neutral_pct ?? 0,
3121
+ negative: analytics.sentiments.negative_pct ?? 0
3122
+ }
3123
+ };
3124
+ }
3125
+ function delay3(ms) {
3126
+ return new Promise((resolve) => setTimeout(resolve, ms));
3127
+ }
3128
+ async function* normalizeTranscripts(transcripts, options) {
3129
+ const { delayMs = 0, ...normalizationOptions } = options ?? {};
3130
+ let isFirst = true;
3131
+ for await (const transcript of transcripts) {
3132
+ if (!isFirst && delayMs > 0) {
3133
+ await delay3(delayMs);
3134
+ }
3135
+ isFirst = false;
3136
+ try {
3137
+ const result = normalizeTranscript2(transcript, normalizationOptions);
3138
+ yield { item: transcript, result };
3139
+ } catch (err) {
3140
+ yield {
3141
+ item: transcript,
3142
+ error: err instanceof Error ? err : new Error(String(err))
3143
+ };
3144
+ }
3145
+ }
3146
+ }
3147
+ async function normalizeTranscriptsAll(transcripts, options) {
3148
+ const results = [];
3149
+ for await (const result of normalizeTranscripts(transcripts, options)) {
3150
+ results.push(result);
3151
+ }
3152
+ return results;
3153
+ }
3154
+
3155
+ // src/helpers/speaker-analytics.ts
3156
+ function analyzeSpeakers(transcript, options = {}) {
3157
+ const {
3158
+ mergeSpeakersByName = true,
3159
+ roundPercentages = true,
3160
+ unbalancedThreshold = 40,
3161
+ dominatedThreshold = 60
3162
+ } = options;
3163
+ const sentences = transcript.sentences ?? [];
3164
+ if (sentences.length === 0) {
3165
+ return emptyAnalytics();
3166
+ }
3167
+ const speakerMap = /* @__PURE__ */ new Map();
3168
+ let prevSpeakerKey = null;
3169
+ for (const sentence of sentences) {
3170
+ const groupKey = mergeSpeakersByName ? sentence.speaker_name : sentence.speaker_id;
3171
+ const data = getOrCreateSpeakerData(speakerMap, groupKey, sentence);
3172
+ data.sentences.push(sentence);
3173
+ data.talkTime += parseDuration(sentence);
3174
+ data.wordCount += countWords(sentence.text);
3175
+ if (groupKey !== prevSpeakerKey) {
3176
+ data.turnCount++;
3177
+ prevSpeakerKey = groupKey;
3178
+ }
3179
+ }
3180
+ const totalTalkTime = sumTalkTime(speakerMap);
3181
+ const totalDuration = calculateDuration2(sentences);
3182
+ const totalWords = sumWords(speakerMap);
3183
+ const speakers = buildSpeakerStats(speakerMap, totalTalkTime, roundPercentages);
3184
+ speakers.sort((a, b) => b.talkTime - a.talkTime);
3185
+ const dominant = speakers[0];
3186
+ return {
3187
+ speakers,
3188
+ totalDuration,
3189
+ totalTalkTime,
3190
+ totalSentences: sentences.length,
3191
+ totalWords,
3192
+ dominantSpeaker: dominant?.name ?? "",
3193
+ dominantSpeakerPercentage: dominant?.talkTimePercentage ?? 0,
3194
+ balance: classifyBalance(speakers, unbalancedThreshold, dominatedThreshold)
3195
+ };
3196
+ }
3197
+ function emptyAnalytics() {
3198
+ return {
3199
+ speakers: [],
3200
+ totalDuration: 0,
3201
+ totalTalkTime: 0,
3202
+ totalSentences: 0,
3203
+ totalWords: 0,
3204
+ dominantSpeaker: "",
3205
+ dominantSpeakerPercentage: 0,
3206
+ balance: "balanced"
3207
+ };
3208
+ }
3209
+ function getOrCreateSpeakerData(speakerMap, groupKey, sentence) {
3210
+ let data = speakerMap.get(groupKey);
3211
+ if (!data) {
3212
+ data = {
3213
+ id: sentence.speaker_id,
3214
+ // Use first encountered ID
3215
+ name: sentence.speaker_name,
3216
+ sentences: [],
3217
+ talkTime: 0,
3218
+ wordCount: 0,
3219
+ turnCount: 0
3220
+ };
3221
+ speakerMap.set(groupKey, data);
3222
+ }
3223
+ return data;
3224
+ }
3225
+ function parseDuration(sentence) {
3226
+ const start = Number.parseFloat(sentence.start_time);
3227
+ const end = Number.parseFloat(sentence.end_time);
3228
+ return Math.max(0, end - start);
3229
+ }
3230
+ function countWords(text) {
3231
+ if (!text || text.length === 0) return 0;
3232
+ return text.trim().split(/\s+/).filter((w) => w.length > 0).length;
3233
+ }
3234
+ function sumTalkTime(speakerMap) {
3235
+ let total = 0;
3236
+ for (const data of speakerMap.values()) {
3237
+ total += data.talkTime;
3238
+ }
3239
+ return total;
3240
+ }
3241
+ function sumWords(speakerMap) {
3242
+ let total = 0;
3243
+ for (const data of speakerMap.values()) {
3244
+ total += data.wordCount;
3245
+ }
3246
+ return total;
3247
+ }
3248
+ function calculateDuration2(sentences) {
3249
+ if (sentences.length === 0) return 0;
3250
+ const lastSentence = sentences[sentences.length - 1];
3251
+ return Number.parseFloat(lastSentence?.end_time ?? "0");
3252
+ }
3253
+ function buildSpeakerStats(speakerMap, totalTalkTime, roundPercentages) {
3254
+ const speakers = [];
3255
+ for (const data of speakerMap.values()) {
3256
+ const percentage = totalTalkTime > 0 ? data.talkTime / totalTalkTime * 100 : 0;
3257
+ const sentenceCount = data.sentences.length;
3258
+ const talkTimeMinutes = data.talkTime / 60;
3259
+ const wordsPerMinute = talkTimeMinutes > 0 ? data.wordCount / talkTimeMinutes : 0;
3260
+ const averageSentenceLength = sentenceCount > 0 ? data.wordCount / sentenceCount : 0;
3261
+ speakers.push({
3262
+ name: data.name,
3263
+ id: data.id,
3264
+ talkTime: data.talkTime,
3265
+ talkTimePercentage: roundPercentages ? Math.round(percentage) : percentage,
3266
+ sentenceCount,
3267
+ wordCount: data.wordCount,
3268
+ wordsPerMinute: roundPercentages ? Math.round(wordsPerMinute) : wordsPerMinute,
3269
+ averageSentenceLength,
3270
+ turnCount: data.turnCount
3271
+ });
3272
+ }
3273
+ return speakers;
3274
+ }
3275
+ function classifyBalance(speakers, unbalancedThreshold, dominatedThreshold) {
3276
+ if (speakers.length <= 2) return "balanced";
3277
+ const top = speakers[0]?.talkTimePercentage ?? 0;
3278
+ if (top > dominatedThreshold) return "dominated";
3279
+ if (top > unbalancedThreshold) return "unbalanced";
3280
+ return "balanced";
3281
+ }
3282
+
3283
+ // src/helpers/videos.ts
3284
+ async function* getMeetingVideos(client, options) {
3285
+ for await (const transcript of client.transcripts.listAll(options)) {
3286
+ if (transcript.video_url) {
3287
+ yield {
3288
+ transcript,
3289
+ videoUrl: transcript.video_url
3290
+ };
3291
+ }
3292
+ }
3293
+ }
3294
+ function hasVideo(transcript) {
3295
+ return typeof transcript.video_url === "string" && transcript.video_url.length > 0;
3296
+ }
3297
+ function verifyWebhookSignature(options) {
3298
+ const { payload, signature, secret } = options;
3299
+ if (!signature || !secret) {
3300
+ return false;
3301
+ }
3302
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
3303
+ const computed = crypto.createHmac("sha256", secret).update(payloadString).digest("hex");
3304
+ try {
3305
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(computed));
3306
+ } catch {
3307
+ return false;
3308
+ }
3309
+ }
3310
+
3311
+ // src/webhooks/parse.ts
3312
+ var VALID_EVENT_TYPES = ["Transcription completed"];
3313
+ function isValidEventType(value) {
3314
+ return VALID_EVENT_TYPES.includes(value);
3315
+ }
3316
+ function isValidWebhookPayload(payload) {
3317
+ if (!payload || typeof payload !== "object") {
3318
+ return false;
3319
+ }
3320
+ const meetingId = payload["meetingId"];
3321
+ const eventType = payload["eventType"];
3322
+ const clientReferenceId = payload["clientReferenceId"];
3323
+ return typeof meetingId === "string" && typeof eventType === "string" && isValidEventType(eventType) && (clientReferenceId === void 0 || typeof clientReferenceId === "string");
3324
+ }
3325
+ function parseWebhookPayload(payload, options) {
3326
+ if (options?.signature && options?.secret) {
3327
+ const isValid = verifyWebhookSignature({
3328
+ payload,
3329
+ signature: options.signature,
3330
+ secret: options.secret
3331
+ });
3332
+ if (!isValid) {
3333
+ throw new WebhookVerificationError("Invalid webhook signature");
3334
+ }
3335
+ }
3336
+ if (!isValidWebhookPayload(payload)) {
3337
+ throw new WebhookParseError(
3338
+ "Invalid webhook payload: expected meetingId (string), eventType (valid event type), and optional clientReferenceId (string)"
3339
+ );
3340
+ }
3341
+ return payload;
3342
+ }
3343
+
3344
+ exports.AuthenticationError = AuthenticationError;
3345
+ exports.ChunkTimeoutError = ChunkTimeoutError;
3346
+ exports.ConnectionError = ConnectionError;
3347
+ exports.Deduplicator = Deduplicator;
3348
+ exports.FirefliesClient = FirefliesClient;
3349
+ exports.FirefliesError = FirefliesError;
3350
+ exports.GraphQLError = GraphQLError;
3351
+ exports.NetworkError = NetworkError;
3352
+ exports.NotFoundError = NotFoundError;
3353
+ exports.RateLimitError = RateLimitError;
3354
+ exports.RealtimeError = RealtimeError;
3355
+ exports.RealtimeStream = RealtimeStream;
3356
+ exports.StreamClosedError = StreamClosedError;
3357
+ exports.TimeoutError = TimeoutError;
3358
+ exports.TranscriptAccumulator = TranscriptAccumulator;
3359
+ exports.ValidationError = ValidationError;
3360
+ exports.WebhookParseError = WebhookParseError;
3361
+ exports.WebhookVerificationError = WebhookVerificationError;
3362
+ exports.aggregateActionItems = aggregateActionItems;
3363
+ exports.analyzeMeetings = analyzeMeetings;
3364
+ exports.analyzeSpeakers = analyzeSpeakers;
3365
+ exports.batch = batch;
3366
+ exports.batchAll = batchAll;
3367
+ exports.chunksToMarkdown = chunksToMarkdown;
3368
+ exports.collectAll = collectAll;
3369
+ exports.createNormalizer = createNormalizer;
3370
+ exports.extractActionItems = extractActionItems;
3371
+ exports.extractDomain = extractDomain;
3372
+ exports.filterActionItems = filterActionItems;
3373
+ exports.findExternalParticipantQuestions = findExternalParticipantQuestions;
3374
+ exports.formatActionItemsMarkdown = formatActionItemsMarkdown;
3375
+ exports.getMeetingVideos = getMeetingVideos;
3376
+ exports.getMeetingsForMultipleUsers = getMeetingsForMultipleUsers;
3377
+ exports.hasExternalParticipants = hasExternalParticipants;
3378
+ exports.hasVideo = hasVideo;
3379
+ exports.isValidWebhookPayload = isValidWebhookPayload;
3380
+ exports.normalizeTranscript = normalizeTranscript2;
3381
+ exports.normalizeTranscripts = normalizeTranscripts;
3382
+ exports.normalizeTranscriptsAll = normalizeTranscriptsAll;
3383
+ exports.paginate = paginate;
3384
+ exports.parseWebhookPayload = parseWebhookPayload;
3385
+ exports.searchTranscript = searchTranscript;
3386
+ exports.transcriptToMarkdown = transcriptToMarkdown;
3387
+ exports.verifyWebhookSignature = verifyWebhookSignature;
3388
+ //# sourceMappingURL=index.cjs.map
3389
+ //# sourceMappingURL=index.cjs.map