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