@griffinwork40/clickup-mcp-server 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared utility functions for ClickUp MCP server.
3
3
  */
4
- import type { ClickUpTask, ClickUpList, ClickUpSpace, ClickUpFolder, ClickUpComment, ClickUpTimeEntry, PaginationInfo, TruncationInfo } from "./types.js";
4
+ import type { ClickUpTask, ClickUpList, ClickUpSpace, ClickUpFolder, ClickUpComment, ClickUpTimeEntry, PaginationInfo, TruncationInfo, ClickUpCustomField } from "./types.js";
5
5
  /**
6
6
  * Get ClickUp API token from environment
7
7
  */
@@ -63,6 +63,7 @@ export declare function getPagination(total: number | undefined, count: number,
63
63
  export declare function generateTaskSummary(tasks: ClickUpTask[]): string;
64
64
  /**
65
65
  * Truncate response if it exceeds character limit with smart boundary detection
66
+ * Detects format and uses appropriate truncation strategy
66
67
  */
67
68
  export declare function truncateResponse(content: string, itemCount: number, itemType?: string): {
68
69
  content: string;
@@ -72,4 +73,69 @@ export declare function truncateResponse(content: string, itemCount: number, ite
72
73
  * Format truncation information as text
73
74
  */
74
75
  export declare function formatTruncationInfo(truncation: TruncationInfo | null): string;
76
+ /**
77
+ * Filter tasks by status names (client-side filtering)
78
+ */
79
+ export declare function filterTasksByStatus(tasks: ClickUpTask[], statuses: string[]): ClickUpTask[];
80
+ /**
81
+ * Options for counting tasks by status
82
+ */
83
+ export interface CountTasksOptions {
84
+ archived?: boolean;
85
+ include_closed?: boolean;
86
+ statuses?: string[];
87
+ }
88
+ /**
89
+ * Result of counting tasks by status
90
+ */
91
+ export interface CountTasksResult {
92
+ total: number;
93
+ by_status: Record<string, number>;
94
+ }
95
+ /**
96
+ * Count tasks in a list by status, handling pagination internally
97
+ */
98
+ export declare function countTasksByStatus(listId: string, options?: CountTasksOptions): Promise<CountTasksResult>;
99
+ /**
100
+ * Options for exporting tasks to CSV
101
+ */
102
+ export interface ExportCSVOptions {
103
+ archived?: boolean;
104
+ include_closed?: boolean;
105
+ statuses?: string[];
106
+ custom_fields?: string[];
107
+ include_standard_fields?: boolean;
108
+ add_phone_number_column?: boolean;
109
+ }
110
+ /**
111
+ * Escape CSV fields to handle commas, quotes, and newlines
112
+ */
113
+ export declare function escapeCSV(value: any): string;
114
+ /**
115
+ * Normalize phone number to E.164 format
116
+ * E.164 format: ^\+?[1-9]\d{1,14}$
117
+ * Rules:
118
+ * - Optional + at the start
119
+ * - Must start with digit 1-9 (not 0)
120
+ * - Followed by 1-14 digits only
121
+ * - NO spaces, dashes, parentheses, dots, or other characters
122
+ * - NO extensions
123
+ */
124
+ export declare function normalizePhoneToE164(phone: string | null | undefined): string;
125
+ /**
126
+ * Extract value from a custom field based on its type
127
+ */
128
+ export declare function extractCustomFieldValue(field: ClickUpCustomField): string;
129
+ /**
130
+ * Get custom field value by name (prefers field with value if multiple exist)
131
+ */
132
+ export declare function getCustomField(task: ClickUpTask, fieldName: string, preferredType?: string): string;
133
+ /**
134
+ * Convert task to CSV row array
135
+ */
136
+ export declare function taskToCSVRow(task: ClickUpTask, fieldOrder: string[]): string[];
137
+ /**
138
+ * Export tasks to CSV format
139
+ */
140
+ export declare function exportTasksToCSV(listId: string, options?: ExportCSVOptions): Promise<string>;
75
141
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAgB,EACjD,IAAI,CAAC,EAAE,GAAG,EACV,MAAM,CAAC,EAAE,GAAG,GACX,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAgCrD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAKzE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGrF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAkC5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAM3D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAwB5D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAkB/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAqBlE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAWrE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CA2BvE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,cAAc,CAUhB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAqDhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,GAAE,MAAgB,GACzB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;CAAE,CA0CxD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,CAI9E"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/dist/utils.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Shared utility functions for ClickUp MCP server.
3
3
  */
4
4
  import axios, { AxiosError } from "axios";
5
- import { API_BASE_URL, CHARACTER_LIMIT, DEFAULT_TIMEOUT } from "./constants.js";
5
+ import { API_BASE_URL, CHARACTER_LIMIT, DEFAULT_TIMEOUT, MAX_LIMIT } from "./constants.js";
6
6
  /**
7
7
  * Get ClickUp API token from environment
8
8
  */
@@ -293,9 +293,71 @@ export function generateTaskSummary(tasks) {
293
293
  return lines.join("\n");
294
294
  }
295
295
  /**
296
- * Truncate response if it exceeds character limit with smart boundary detection
296
+ * Truncate JSON response by removing items from arrays
297
297
  */
298
- export function truncateResponse(content, itemCount, itemType = "items") {
298
+ function truncateJsonResponse(content, itemCount, itemType = "items") {
299
+ try {
300
+ const data = JSON.parse(content);
301
+ // Find the main array (tasks, conversations, etc.)
302
+ const arrayKey = Object.keys(data).find(key => Array.isArray(data[key]));
303
+ if (!arrayKey || !Array.isArray(data[arrayKey])) {
304
+ // If no array found, fall back to string truncation
305
+ throw new Error('No array found in JSON');
306
+ }
307
+ const items = data[arrayKey];
308
+ let keptItems = items.length;
309
+ // Remove items one by one until we're under the limit
310
+ while (JSON.stringify(data).length > CHARACTER_LIMIT && data[arrayKey].length > 1) {
311
+ data[arrayKey].pop();
312
+ keptItems = data[arrayKey].length;
313
+ }
314
+ const finalContent = JSON.stringify(data, null, 2);
315
+ // Check if even a single item exceeds the limit
316
+ if (finalContent.length > CHARACTER_LIMIT) {
317
+ // Single item is too large - truncate the item's description/content fields
318
+ if (data[arrayKey].length === 1 && typeof data[arrayKey][0] === 'object') {
319
+ const item = data[arrayKey][0];
320
+ // Truncate large text fields
321
+ for (const key of Object.keys(item)) {
322
+ if (typeof item[key] === 'string' && item[key].length > 10000) {
323
+ item[key] = item[key].substring(0, 10000) + '... [truncated]';
324
+ }
325
+ }
326
+ // Retry serialization
327
+ const compactContent = JSON.stringify(data, null, 2);
328
+ if (compactContent.length <= CHARACTER_LIMIT) {
329
+ const truncation = {
330
+ truncated: true,
331
+ original_count: itemCount,
332
+ returned_count: 1,
333
+ truncation_message: `Large ${itemType} fields were truncated to fit size limits (${CHARACTER_LIMIT.toLocaleString()} chars).`
334
+ };
335
+ return { content: compactContent, truncation };
336
+ }
337
+ }
338
+ // Still too large - fall back to markdown truncation
339
+ return truncateMarkdownResponse(content, itemCount, itemType);
340
+ }
341
+ if (keptItems < items.length) {
342
+ const truncation = {
343
+ truncated: true,
344
+ original_count: items.length,
345
+ returned_count: keptItems,
346
+ truncation_message: `Response truncated from ${items.length} to ${keptItems} ${itemType} due to size limits (${CHARACTER_LIMIT.toLocaleString()} chars). Use pagination (offset/limit), add filters, or use response_mode='compact' to see more results.`
347
+ };
348
+ return { content: finalContent, truncation };
349
+ }
350
+ return { content: finalContent, truncation: null };
351
+ }
352
+ catch {
353
+ // If JSON parsing fails, fall back to string truncation
354
+ return truncateMarkdownResponse(content, itemCount, itemType);
355
+ }
356
+ }
357
+ /**
358
+ * Truncate markdown response if it exceeds character limit with smart boundary detection
359
+ */
360
+ function truncateMarkdownResponse(content, itemCount, itemType = "items") {
299
361
  if (content.length <= CHARACTER_LIMIT) {
300
362
  return { content, truncation: null };
301
363
  }
@@ -332,6 +394,21 @@ export function truncateResponse(content, itemCount, itemType = "items") {
332
394
  };
333
395
  return { content: finalContent, truncation };
334
396
  }
397
+ /**
398
+ * Truncate response if it exceeds character limit with smart boundary detection
399
+ * Detects format and uses appropriate truncation strategy
400
+ */
401
+ export function truncateResponse(content, itemCount, itemType = "items") {
402
+ if (content.length <= CHARACTER_LIMIT) {
403
+ return { content, truncation: null };
404
+ }
405
+ // Detect if content is JSON by checking first non-whitespace character
406
+ const trimmed = content.trimStart();
407
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
408
+ return truncateJsonResponse(content, itemCount, itemType);
409
+ }
410
+ return truncateMarkdownResponse(content, itemCount, itemType);
411
+ }
335
412
  /**
336
413
  * Format truncation information as text
337
414
  */
@@ -340,4 +417,380 @@ export function formatTruncationInfo(truncation) {
340
417
  return "";
341
418
  return `\n\n---\n⚠️ ${truncation.truncation_message}`;
342
419
  }
420
+ /**
421
+ * Filter tasks by status names (client-side filtering)
422
+ */
423
+ export function filterTasksByStatus(tasks, statuses) {
424
+ if (!statuses || statuses.length === 0) {
425
+ return tasks;
426
+ }
427
+ return tasks.filter(task => {
428
+ const taskStatus = task.status?.status;
429
+ return taskStatus && statuses.includes(taskStatus);
430
+ });
431
+ }
432
+ /**
433
+ * Count tasks in a list by status, handling pagination internally
434
+ */
435
+ export async function countTasksByStatus(listId, options = {}) {
436
+ const { archived = false, include_closed = false, statuses } = options;
437
+ const limit = 100; // Use max limit for efficiency
438
+ let offset = 0;
439
+ let allTasks = [];
440
+ let hasMore = true;
441
+ // Fetch all tasks with pagination
442
+ while (hasMore) {
443
+ const queryParams = {
444
+ archived,
445
+ include_closed,
446
+ page: Math.floor(offset / limit)
447
+ };
448
+ try {
449
+ const data = await makeApiRequest(`list/${listId}/task`, "GET", undefined, queryParams);
450
+ const tasks = data.tasks || [];
451
+ allTasks.push(...tasks);
452
+ hasMore = tasks.length === limit;
453
+ offset += limit;
454
+ }
455
+ catch (error) {
456
+ throw error;
457
+ }
458
+ }
459
+ // Filter by status if specified
460
+ let filteredTasks = allTasks;
461
+ if (statuses && statuses.length > 0) {
462
+ filteredTasks = filterTasksByStatus(allTasks, statuses);
463
+ }
464
+ // Count by status
465
+ const byStatus = {};
466
+ for (const task of filteredTasks) {
467
+ const status = task.status?.status || "Unknown";
468
+ byStatus[status] = (byStatus[status] || 0) + 1;
469
+ }
470
+ return {
471
+ total: filteredTasks.length,
472
+ by_status: byStatus
473
+ };
474
+ }
475
+ /**
476
+ * Escape CSV fields to handle commas, quotes, and newlines
477
+ */
478
+ export function escapeCSV(value) {
479
+ if (value === null || value === undefined)
480
+ return '';
481
+ const stringValue = String(value);
482
+ // If contains comma, quote, or newline, wrap in quotes and escape quotes
483
+ if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
484
+ return `"${stringValue.replace(/"/g, '""')}"`;
485
+ }
486
+ return stringValue;
487
+ }
488
+ /**
489
+ * Normalize phone number to E.164 format
490
+ * E.164 format: ^\+?[1-9]\d{1,14}$
491
+ * Rules:
492
+ * - Optional + at the start
493
+ * - Must start with digit 1-9 (not 0)
494
+ * - Followed by 1-14 digits only
495
+ * - NO spaces, dashes, parentheses, dots, or other characters
496
+ * - NO extensions
497
+ */
498
+ export function normalizePhoneToE164(phone) {
499
+ if (!phone)
500
+ return '';
501
+ let normalized = String(phone).trim();
502
+ if (!normalized)
503
+ return '';
504
+ // Remove extensions (x206, ext 123, extension 456, etc.)
505
+ // Match: x, ext, extension followed by optional space and digits
506
+ normalized = normalized.replace(/\s*(?:x|ext|extension)\s*\d+/i, '');
507
+ // Check if it starts with + (preserve it)
508
+ const hasPlus = normalized.startsWith('+');
509
+ if (hasPlus) {
510
+ normalized = normalized.substring(1);
511
+ }
512
+ // Remove all non-digit characters
513
+ normalized = normalized.replace(/\D/g, '');
514
+ // If empty after cleaning, return empty
515
+ if (!normalized)
516
+ return '';
517
+ // If number starts with 0, it's invalid for E.164 (must start with 1-9)
518
+ if (normalized.startsWith('0')) {
519
+ return '';
520
+ }
521
+ // Check if it already has a country code (starts with 1-9 and has 10+ digits)
522
+ // If it's 10 digits and starts with 1, assume it's US/Canada with country code
523
+ // If it's 10 digits and doesn't start with 1, assume it's missing country code
524
+ // If it's 11 digits and starts with 1, it already has country code
525
+ // If it's more than 11 digits, assume it already has country code
526
+ if (normalized.length === 10) {
527
+ // 10 digits without country code - add +1 for US/Canada
528
+ normalized = '1' + normalized;
529
+ }
530
+ else if (normalized.length === 11 && normalized.startsWith('1')) {
531
+ // Already has US/Canada country code
532
+ // Keep as is
533
+ }
534
+ else if (normalized.length < 10) {
535
+ // Too short to be a valid phone number
536
+ return '';
537
+ }
538
+ else if (normalized.length > 15) {
539
+ // Too long for E.164 (max 15 digits after country code)
540
+ return '';
541
+ }
542
+ // Add + prefix
543
+ normalized = '+' + normalized;
544
+ // Validate E.164 format: ^\+?[1-9]\d{1,14}$
545
+ // We already have the +, so check: [1-9]\d{1,14}
546
+ const e164Regex = /^\+[1-9]\d{1,14}$/;
547
+ if (!e164Regex.test(normalized)) {
548
+ return '';
549
+ }
550
+ return normalized;
551
+ }
552
+ /**
553
+ * Extract value from a custom field based on its type
554
+ */
555
+ export function extractCustomFieldValue(field) {
556
+ if (!field || field.value === null || field.value === undefined || field.value === '')
557
+ return '';
558
+ // Check if this is a phone number field (by type or name)
559
+ const isPhoneField = field.type === 'phone' ||
560
+ field.type === 'phone_number' ||
561
+ (typeof field.name === 'string' && /phone/i.test(field.name));
562
+ // Handle different field types
563
+ if (field.type === 'email' || field.type === 'url' || field.type === 'text' || field.type === 'short_text') {
564
+ const value = String(field.value).trim();
565
+ // Normalize if it's a phone field
566
+ return isPhoneField ? normalizePhoneToE164(value) : value;
567
+ }
568
+ else if (field.type === 'phone' || field.type === 'phone_number') {
569
+ return normalizePhoneToE164(field.value);
570
+ }
571
+ else if (field.type === 'number' || field.type === 'currency') {
572
+ return String(field.value);
573
+ }
574
+ else if (field.type === 'date') {
575
+ return new Date(parseInt(field.value)).toISOString().split('T')[0];
576
+ }
577
+ else if (field.type === 'dropdown') {
578
+ return field.value?.label || field.value?.name || String(field.value || '');
579
+ }
580
+ else if (field.type === 'labels') {
581
+ return Array.isArray(field.value) ? field.value.map((v) => v.label || v.name || v).join('; ') : '';
582
+ }
583
+ else if (field.type === 'checklist') {
584
+ return Array.isArray(field.value) ? field.value.map((item) => item.name).join('; ') : '';
585
+ }
586
+ else if (field.type === 'checkbox') {
587
+ return field.value ? 'Yes' : 'No';
588
+ }
589
+ // Default: return trimmed string, normalize if it's a phone field
590
+ const value = String(field.value || '').trim();
591
+ return isPhoneField ? normalizePhoneToE164(value) : value;
592
+ }
593
+ /**
594
+ * Get custom field value by name (prefers field with value if multiple exist)
595
+ */
596
+ export function getCustomField(task, fieldName, preferredType) {
597
+ if (!task.custom_fields)
598
+ return '';
599
+ // Find all fields with matching name
600
+ const matchingFields = task.custom_fields.filter(f => f.name === fieldName);
601
+ if (matchingFields.length === 0)
602
+ return '';
603
+ // If preferred type specified, try that first
604
+ if (preferredType) {
605
+ const typedField = matchingFields.find(f => f.type === preferredType && f.value);
606
+ if (typedField) {
607
+ return extractCustomFieldValue(typedField);
608
+ }
609
+ }
610
+ // Prefer field with a value
611
+ const fieldWithValue = matchingFields.find(f => f.value !== null && f.value !== undefined && f.value !== '');
612
+ const field = fieldWithValue || matchingFields[0];
613
+ return extractCustomFieldValue(field);
614
+ }
615
+ /**
616
+ * Convert task to CSV row array
617
+ */
618
+ export function taskToCSVRow(task, fieldOrder) {
619
+ const row = [];
620
+ for (const fieldName of fieldOrder) {
621
+ let value = '';
622
+ // Standard fields
623
+ if (fieldName === 'Task ID') {
624
+ value = task.id;
625
+ }
626
+ else if (fieldName === 'Name') {
627
+ value = task.name;
628
+ }
629
+ else if (fieldName === 'Status') {
630
+ value = task.status?.status || '';
631
+ }
632
+ else if (fieldName === 'Date Created') {
633
+ value = task.date_created ? new Date(parseInt(task.date_created)).toISOString() : '';
634
+ }
635
+ else if (fieldName === 'Date Updated') {
636
+ value = task.date_updated ? new Date(parseInt(task.date_updated)).toISOString() : '';
637
+ }
638
+ else if (fieldName === 'URL') {
639
+ value = task.url || '';
640
+ }
641
+ else if (fieldName === 'Assignees') {
642
+ value = task.assignees?.map(a => a.username || a.email).join('; ') || '';
643
+ }
644
+ else if (fieldName === 'Creator') {
645
+ value = task.creator?.username || task.creator?.email || '';
646
+ }
647
+ else if (fieldName === 'Due Date') {
648
+ value = task.due_date ? new Date(parseInt(task.due_date)).toISOString() : '';
649
+ }
650
+ else if (fieldName === 'Priority') {
651
+ value = task.priority?.priority || '';
652
+ }
653
+ else if (fieldName === 'Description') {
654
+ value = task.description || task.text_content || '';
655
+ }
656
+ else if (fieldName === 'Tags') {
657
+ value = task.tags?.map(t => t.name).join('; ') || '';
658
+ }
659
+ else if (fieldName === 'phone_number') {
660
+ // Combined phone number field for ElevenLabs compatibility
661
+ // First check if a real phone_number custom field exists
662
+ const realPhoneNumberField = task.custom_fields?.find(f => f.name === 'phone_number');
663
+ if (realPhoneNumberField && (realPhoneNumberField.value !== null && realPhoneNumberField.value !== undefined && realPhoneNumberField.value !== '')) {
664
+ // Use the real phone_number field value (already normalized)
665
+ value = extractCustomFieldValue(realPhoneNumberField);
666
+ }
667
+ else {
668
+ // No real phone_number field, use synthetic logic
669
+ // Priority: Personal Phone > Biz Phone number > first phone field found
670
+ const personalPhone = getCustomField(task, 'Personal Phone');
671
+ const bizPhone = getCustomField(task, 'Biz Phone number');
672
+ if (personalPhone) {
673
+ value = personalPhone;
674
+ }
675
+ else if (bizPhone) {
676
+ value = bizPhone;
677
+ }
678
+ else {
679
+ // Try to find any phone field
680
+ const phoneFields = task.custom_fields?.filter(f => f.type === 'phone' ||
681
+ f.type === 'phone_number' ||
682
+ (typeof f.name === 'string' && /phone/i.test(f.name))) || [];
683
+ if (phoneFields.length > 0) {
684
+ value = extractCustomFieldValue(phoneFields[0]);
685
+ }
686
+ }
687
+ }
688
+ }
689
+ else {
690
+ // Custom field
691
+ value = getCustomField(task, fieldName);
692
+ }
693
+ row.push(escapeCSV(value));
694
+ }
695
+ return row;
696
+ }
697
+ /**
698
+ * Export tasks to CSV format
699
+ */
700
+ export async function exportTasksToCSV(listId, options = {}) {
701
+ const { archived = false, include_closed = false, statuses, custom_fields, include_standard_fields = true, add_phone_number_column = false } = options;
702
+ const limit = MAX_LIMIT;
703
+ let offset = 0;
704
+ let allTasks = [];
705
+ let hasMore = true;
706
+ // Step 1: Fetch all tasks with pagination
707
+ while (hasMore) {
708
+ const queryParams = {
709
+ archived,
710
+ include_closed,
711
+ page: Math.floor(offset / limit)
712
+ };
713
+ try {
714
+ const data = await makeApiRequest(`list/${listId}/task`, "GET", undefined, queryParams);
715
+ const tasks = data.tasks || [];
716
+ allTasks.push(...tasks);
717
+ hasMore = tasks.length === limit;
718
+ offset += limit;
719
+ }
720
+ catch (error) {
721
+ throw error;
722
+ }
723
+ }
724
+ // Step 2: Filter by status if specified
725
+ if (statuses && statuses.length > 0) {
726
+ allTasks = filterTasksByStatus(allTasks, statuses);
727
+ }
728
+ if (allTasks.length === 0) {
729
+ return ''; // Return empty CSV if no tasks
730
+ }
731
+ // Note: The list endpoint already returns custom fields with values,
732
+ // so we don't need to fetch individual task details!
733
+ // Step 3: Build field order and headers
734
+ const standardFields = [
735
+ 'Task ID',
736
+ 'Name',
737
+ 'Status',
738
+ 'Date Created',
739
+ 'Date Updated',
740
+ 'URL',
741
+ 'Assignees',
742
+ 'Creator',
743
+ 'Due Date',
744
+ 'Priority',
745
+ 'Description',
746
+ 'Tags'
747
+ ];
748
+ // Collect all custom field names from tasks
749
+ const allCustomFieldNames = new Set();
750
+ for (const task of allTasks) {
751
+ if (task.custom_fields) {
752
+ for (const field of task.custom_fields) {
753
+ allCustomFieldNames.add(field.name);
754
+ }
755
+ }
756
+ }
757
+ // Determine which custom fields to include
758
+ const customFieldsToInclude = custom_fields && custom_fields.length > 0
759
+ ? custom_fields.filter(name => allCustomFieldNames.has(name))
760
+ : Array.from(allCustomFieldNames);
761
+ // Build field order
762
+ const fieldOrder = [];
763
+ if (include_standard_fields) {
764
+ fieldOrder.push(...standardFields);
765
+ }
766
+ fieldOrder.push(...customFieldsToInclude);
767
+ // Add phone_number column if requested (for ElevenLabs compatibility)
768
+ if (add_phone_number_column) {
769
+ // Check if phone_number already exists (e.g., as a real custom field)
770
+ const phoneNumberIdx = fieldOrder.indexOf('phone_number');
771
+ if (phoneNumberIdx === -1) {
772
+ // phone_number doesn't exist, add our synthetic one
773
+ // Find Email index to insert after it
774
+ const emailIdx = fieldOrder.indexOf('Email');
775
+ if (emailIdx >= 0) {
776
+ fieldOrder.splice(emailIdx + 1, 0, 'phone_number');
777
+ }
778
+ else {
779
+ // If no Email field, add at the end
780
+ fieldOrder.push('phone_number');
781
+ }
782
+ }
783
+ // If phone_number already exists, use the existing one (no duplicate needed)
784
+ }
785
+ // Step 4: Build CSV
786
+ const csvRows = [];
787
+ // Headers
788
+ csvRows.push(fieldOrder.map(escapeCSV).join(','));
789
+ // Data rows
790
+ for (const task of allTasks) {
791
+ const row = taskToCSVRow(task, fieldOrder);
792
+ csvRows.push(row.join(','));
793
+ }
794
+ return csvRows.join('\n');
795
+ }
343
796
  //# sourceMappingURL=utils.js.map