@nathanvale/chatline 0.2.1 → 0.3.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.
@@ -0,0 +1,688 @@
1
+ /**
2
+ * TypeScript type for the full configuration
3
+ * Explicitly defined for DTS generation compatibility
4
+ */
5
+ type Config = {
6
+ version: string;
7
+ attachmentRoots: string[];
8
+ gemini: {
9
+ apiKey: string;
10
+ model: string;
11
+ rateLimitDelay: number;
12
+ maxRetries: number;
13
+ };
14
+ firecrawl?: {
15
+ apiKey?: string;
16
+ enabled: boolean;
17
+ };
18
+ enrichment: {
19
+ enableVisionAnalysis: boolean;
20
+ enableAudioTranscription: boolean;
21
+ enableLinkEnrichment: boolean;
22
+ imageCacheDir: string;
23
+ checkpointInterval: number;
24
+ forceRefresh: boolean;
25
+ };
26
+ render: {
27
+ groupByTimeOfDay: boolean;
28
+ renderRepliesAsNested: boolean;
29
+ renderTapbacksAsEmoji: boolean;
30
+ maxNestingDepth: number;
31
+ };
32
+ };
33
+ /**
34
+ * CONFIG-T01-AC05: Validate config with detailed error messages
35
+ *
36
+ * @param config - Raw config object (parsed from JSON/YAML)
37
+ * @returns Validated and typed config object
38
+ * @throws ZodError with field paths and expected types
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * try {
43
+ * const config = validateConfig(rawConfig)
44
+ * } catch (error) {
45
+ * if (error instanceof z.ZodError) {
46
+ * error.errors.forEach(err => {
47
+ * console.error(`${err.path.join('.')}: ${err.message}`)
48
+ * })
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ declare function validateConfig2(config: unknown): Config;
54
+ /**
55
+ * Validate config and return result with detailed errors
56
+ *
57
+ * @param config - Raw config object
58
+ * @returns Validation result with success flag and data/errors
59
+ */
60
+ declare function validateConfigSafe(config: unknown): {
61
+ success: boolean;
62
+ data?: Config;
63
+ errors?: Array<{
64
+ path: string;
65
+ message: string;
66
+ }>;
67
+ };
68
+ /**
69
+ * Default configuration values
70
+ *
71
+ * Used as fallback when no config file is present
72
+ */
73
+ declare const DEFAULT_CONFIG: Partial<Config>;
74
+ /**
75
+ * CONFIG-T01-AC04: Config file discovery patterns
76
+ *
77
+ * Checked in order:
78
+ * 1. ./imessage-config.yaml
79
+ * 2. ./imessage-config.yml
80
+ * 3. ./imessage-config.json
81
+ */
82
+ declare const CONFIG_FILE_PATTERNS: readonly ["./imessage-config.yaml", "./imessage-config.yml", "./imessage-config.json"];
83
+ /**
84
+ * CONFIG-T01-AC02: Supported config file formats
85
+ */
86
+ type ConfigFormat = "json" | "yaml";
87
+ /**
88
+ * Detect config file format from file extension
89
+ */
90
+ declare function detectConfigFormat(filePath: string): ConfigFormat;
91
+ /**
92
+ * CONFIG-T03-AC02: Generate config file content in specified format
93
+ *
94
+ * @param format - Output format ('json' or 'yaml')
95
+ * @returns Formatted config file content as string
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const yamlContent = generateConfigContent('yaml')
100
+ * const jsonContent = generateConfigContent('json')
101
+ * ```
102
+ */
103
+ declare function generateConfigContent(format: ConfigFormat): string;
104
+ /**
105
+ * CONFIG-T03-AC05: Validate generated config content
106
+ *
107
+ * Parses the generated content and validates against schema
108
+ * to ensure the template produces valid config.
109
+ *
110
+ * @param content - Generated config content
111
+ * @param format - Format of content ('json' | 'yaml')
112
+ * @returns Validation result
113
+ */
114
+ declare function validateGeneratedConfig(content: string, format: ConfigFormat): {
115
+ valid: boolean;
116
+ errors?: string[];
117
+ };
118
+ /**
119
+ * Get default config file path for a given format
120
+ *
121
+ * @param format - Desired format ('json' | 'yaml')
122
+ * @returns Default file path
123
+ */
124
+ declare function getDefaultConfigPath(format: ConfigFormat): string;
125
+ /**
126
+ * CONFIG-T02-AC01: Discover config file in directory
127
+ *
128
+ * Checks files in order:
129
+ * 1. imessage-config.yaml
130
+ * 2. imessage-config.yml
131
+ * 3. imessage-config.json
132
+ *
133
+ * @param baseDir - Directory to search in (defaults to current directory)
134
+ * @returns Path to first existing config file, or null if none found
135
+ */
136
+ declare function discoverConfigFile(baseDir?: string): Promise<string | null>;
137
+ /**
138
+ * CONFIG-T02-AC01: Load and parse config file
139
+ *
140
+ * Supports JSON and YAML formats with auto-detection
141
+ *
142
+ * @param filePath - Path to config file
143
+ * @returns Parsed config object (unvalidated)
144
+ * @throws Error if file cannot be read or parsed
145
+ */
146
+ declare function loadConfigFile(filePath: string): Promise<unknown>;
147
+ /**
148
+ * CONFIG-T02-AC03: Substitute environment variables in config
149
+ *
150
+ * Recursively replaces ${VAR_NAME} patterns with environment variable values
151
+ *
152
+ * @param obj - Config object (or primitive)
153
+ * @returns Object with env vars substituted
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * // With process.env.GEMINI_API_KEY = 'secret123'
158
+ * substituteEnvVars({ apiKey: '${GEMINI_API_KEY}' })
159
+ * // => { apiKey: 'secret123' }
160
+ * ```
161
+ */
162
+ declare function substituteEnvVars(obj: unknown): unknown;
163
+ /**
164
+ * CONFIG-T02-AC02: Merge configuration with precedence
165
+ *
166
+ * Precedence (highest to lowest):
167
+ * 1. CLI options
168
+ * 2. Config file
169
+ * 3. Defaults (applied by Zod schema)
170
+ *
171
+ * @param fileConfig - Config loaded from file
172
+ * @param cliOptions - Options from CLI flags
173
+ * @returns Merged config object
174
+ */
175
+ declare function mergeConfig(fileConfig: Partial<Config>, cliOptions?: Partial<Config>): Partial<Config>;
176
+ /**
177
+ * CONFIG-T02: Main config loading function
178
+ *
179
+ * Loads configuration with the following precedence:
180
+ * 1. CLI options (highest priority)
181
+ * 2. Config file
182
+ * 3. Defaults from schema (lowest priority)
183
+ *
184
+ * @param options - Loading options
185
+ * @param options.configPath - Explicit config file path (optional)
186
+ * @param options.cliOptions - CLI options to merge (optional)
187
+ * @param options.skipCache - Force reload even if cached (optional)
188
+ * @returns Validated and merged configuration
189
+ * @throws Error if config is invalid or cannot be loaded
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // Load with auto-discovery
194
+ * const config = await loadConfig()
195
+ *
196
+ * // Load specific file
197
+ * const config = await loadConfig({ configPath: './custom-config.yaml' })
198
+ *
199
+ * // Override with CLI options
200
+ * const config = await loadConfig({
201
+ * cliOptions: {
202
+ * gemini: { apiKey: 'override-key' }
203
+ * }
204
+ * })
205
+ * ```
206
+ */
207
+ declare function loadConfig(options?: {
208
+ configPath?: string;
209
+ cliOptions?: Partial<Config>;
210
+ skipCache?: boolean;
211
+ }): Promise<Config>;
212
+ /**
213
+ * Clear the config cache
214
+ *
215
+ * Useful for testing or when config needs to be reloaded
216
+ */
217
+ declare function clearConfigCache(): void;
218
+ /**
219
+ * Check if a config is currently cached
220
+ *
221
+ * @returns True if config is cached
222
+ */
223
+ declare function isConfigCached(): boolean;
224
+ /**
225
+ * Rate Limiting and Retry Logic Module (ENRICH--T07)
226
+ *
227
+ * Implements comprehensive rate limiting with:
228
+ * - AC01: Configurable delays between API calls (default 1000ms)
229
+ * - AC02: Exponential backoff for 429 responses with ±25% jitter
230
+ * - AC03: Retry 5xx errors with maxRetries limit (default 3)
231
+ * - AC04: Respect Retry-After header for 429/503 responses
232
+ * - AC05: Circuit breaker after N consecutive failures (default 5)
233
+ *
234
+ * Architecture:
235
+ * - RateLimiter: Main class managing all rate limiting logic
236
+ * - Configuration with sensible defaults
237
+ * - Per-provider state isolation
238
+ * - Deterministic jitter calculation
239
+ */
240
+ type RateLimitConfig = {
241
+ rateLimitDelay: number;
242
+ maxRetries: number;
243
+ circuitBreakerThreshold: number;
244
+ circuitBreakerResetMs: number;
245
+ };
246
+ type RateLimitState = {
247
+ consecutiveFailures: number;
248
+ circuitOpen: boolean;
249
+ circuitOpenedAt: number | null;
250
+ lastCallTime: number | null;
251
+ };
252
+ type ApiResponse = {
253
+ status: number;
254
+ headers?: Record<string, string | number | undefined>;
255
+ };
256
+ declare class RateLimiter {
257
+ private config;
258
+ private state;
259
+ constructor(partialConfig?: Partial<RateLimitConfig>);
260
+ private validateConfig;
261
+ /**
262
+ * Check if rate limiting should delay the next call
263
+ * @returns delay in ms, or 0 if no delay needed
264
+ */
265
+ shouldRateLimit(): number;
266
+ /**
267
+ * Record a successful API call for rate limiting tracking
268
+ */
269
+ recordCall(): void;
270
+ /**
271
+ * Calculate exponential backoff with ±25% jitter
272
+ * Formula: 2^n seconds ± 25%
273
+ * @param attemptNumber - retry attempt number (1-based)
274
+ * @returns delay in ms
275
+ */
276
+ private calculateExponentialBackoff;
277
+ /**
278
+ * Parse Retry-After header (can be integer seconds or HTTP date)
279
+ * @param retryAfterValue - header value
280
+ * @returns delay in ms, or null if invalid
281
+ */
282
+ private parseRetryAfterHeader;
283
+ /**
284
+ * Determine if response should be retried and calculate delay
285
+ * @param response - API response with status and optional headers
286
+ * @param attemptNumber - current attempt (1-based)
287
+ * @returns { shouldRetry: boolean, delayMs: number }
288
+ */
289
+ getRetryStrategy(response: ApiResponse, attemptNumber: number): {
290
+ shouldRetry: boolean;
291
+ delayMs: number;
292
+ };
293
+ /**
294
+ * Check if we should retry based on attempt count
295
+ * @param attemptNumber - current attempt (1-based)
296
+ * @returns true if we should retry
297
+ */
298
+ shouldRetryAttempt(attemptNumber: number): boolean;
299
+ /**
300
+ * Check if circuit breaker is open
301
+ * @returns true if circuit is open (should fail fast)
302
+ */
303
+ isCircuitOpen(): boolean;
304
+ /**
305
+ * Record a failure for circuit breaker tracking
306
+ */
307
+ recordFailure(): void;
308
+ /**
309
+ * Record a success to reset failure counter
310
+ */
311
+ recordSuccess(): void;
312
+ /**
313
+ * Manually reset circuit breaker
314
+ */
315
+ resetCircuitBreaker(): void;
316
+ /**
317
+ * Get current state for inspection
318
+ */
319
+ getState(): RateLimitState;
320
+ /**
321
+ * Get configuration
322
+ */
323
+ getConfig(): RateLimitConfig;
324
+ /**
325
+ * Reset all state (for testing)
326
+ */
327
+ reset(): void;
328
+ }
329
+ /**
330
+ * Create a new rate limiter with default configuration
331
+ */
332
+ declare function createRateLimiter(config?: Partial<RateLimitConfig>): RateLimiter;
333
+ /**
334
+ * Determine if a status code is a 5xx error (server error)
335
+ */
336
+ declare function is5xx(status: number): boolean;
337
+ /**
338
+ * Determine if a status code should trigger retry
339
+ */
340
+ declare function isRetryableStatus(status: number): boolean;
341
+ import { z as z2 } from "zod";
342
+ type MessageGUID = string;
343
+ type ChatId = string;
344
+ type MediaKind = "image" | "audio" | "video" | "pdf" | "unknown";
345
+ type MediaEnrichment = {
346
+ kind: MediaKind | "link" | "transcription" | "pdf_summary" | "video_metadata" | "link_context" | "image_analysis";
347
+ model?: string;
348
+ createdAt: string;
349
+ visionSummary?: string;
350
+ shortDescription?: string;
351
+ transcription?: string;
352
+ transcript?: string;
353
+ speakers?: string[];
354
+ timestamps?: Array<{
355
+ time: string;
356
+ speaker: string;
357
+ content: string;
358
+ }>;
359
+ pdfSummary?: string;
360
+ videoMetadata?: {
361
+ filename?: string;
362
+ size?: number;
363
+ duration?: number;
364
+ analyzed?: boolean;
365
+ note?: string;
366
+ };
367
+ error?: string;
368
+ usedFallback?: boolean;
369
+ failedProviders?: string[];
370
+ url?: string;
371
+ title?: string;
372
+ summary?: string;
373
+ provider: "gemini" | "firecrawl" | "local" | "youtube" | "spotify" | "twitter" | "instagram" | "generic";
374
+ version: string;
375
+ };
376
+ type MediaProvenance = {
377
+ source: "csv" | "db" | "merged";
378
+ lastSeen: string;
379
+ resolvedAt: string;
380
+ };
381
+ type MediaMeta = {
382
+ id: string;
383
+ filename: string;
384
+ path: string;
385
+ size?: number;
386
+ mimeType?: string;
387
+ uti?: string | null;
388
+ isSticker?: boolean;
389
+ hidden?: boolean;
390
+ mediaKind?: MediaKind;
391
+ enrichment?: Array<MediaEnrichment>;
392
+ provenance?: MediaProvenance;
393
+ };
394
+ type ReplyInfo = {
395
+ sender?: string;
396
+ date?: string;
397
+ text?: string;
398
+ targetMessageGuid?: MessageGUID;
399
+ };
400
+ type TapbackInfo = {
401
+ type: "loved" | "liked" | "disliked" | "laughed" | "emphasized" | "questioned" | "emoji";
402
+ action: "added" | "removed";
403
+ targetMessageGuid?: MessageGUID;
404
+ targetMessagePart?: number;
405
+ targetText?: string;
406
+ isMedia?: boolean;
407
+ emoji?: string;
408
+ };
409
+ type MessageCore = {
410
+ guid: MessageGUID;
411
+ rowid?: number;
412
+ chatId?: ChatId | null;
413
+ service?: string | null;
414
+ subject?: string | null;
415
+ handleId?: number | null;
416
+ handle?: string | null;
417
+ destinationCallerId?: string | null;
418
+ isFromMe: boolean;
419
+ otherHandle?: number | null;
420
+ date: string;
421
+ dateRead?: string | null;
422
+ dateDelivered?: string | null;
423
+ dateEdited?: string | null;
424
+ isRead?: boolean;
425
+ itemType?: number;
426
+ groupActionType?: number;
427
+ groupTitle?: string | null;
428
+ shareStatus?: boolean;
429
+ shareDirection?: boolean | null;
430
+ expressiveSendStyleId?: string | null;
431
+ balloonBundleId?: string | null;
432
+ threadOriginatorGuid?: string | null;
433
+ threadOriginatorPart?: number | null;
434
+ numReplies?: number;
435
+ deletedFrom?: number | null;
436
+ };
437
+ type Message = {
438
+ messageKind: "text" | "media" | "tapback" | "notification";
439
+ text?: string | null;
440
+ tapback?: TapbackInfo | null;
441
+ replyingTo?: ReplyInfo | null;
442
+ replyingToRaw?: string | null;
443
+ media?: MediaMeta | null;
444
+ groupGuid?: string | null;
445
+ exportTimestamp?: string;
446
+ exportVersion?: string;
447
+ isUnsent?: boolean;
448
+ isEdited?: boolean;
449
+ } & MessageCore;
450
+ type ExportEnvelope = {
451
+ schemaVersion: string;
452
+ source: "csv" | "db" | "merged";
453
+ createdAt: string;
454
+ messages: Array<Message>;
455
+ meta?: Record<string, unknown>;
456
+ };
457
+ declare const MediaEnrichmentSchema: z2.ZodType<MediaEnrichment>;
458
+ declare const MediaProvenanceSchema: z2.ZodType<MediaProvenance>;
459
+ declare const MediaMetaSchema: z2.ZodType<MediaMeta>;
460
+ declare const ReplyInfoSchema: z2.ZodType<ReplyInfo>;
461
+ declare const TapbackInfoSchema: z2.ZodType<TapbackInfo>;
462
+ declare const MessageCoreSchema: z2.ZodType<MessageCore>;
463
+ /**
464
+ * Deduplication and merge logic for NORMALIZE--T04
465
+ *
466
+ * Merges CSV and DB message sources with:
467
+ * AC01: Exact GUID matching (primary)
468
+ * AC02: DB authoritiveness for conflicts
469
+ * AC03: Content equivalence detection
470
+ * AC04: Data loss verification
471
+ * AC05: Deterministic GUID assignment
472
+ */
473
+ type MergeStats = {
474
+ csvCount: number;
475
+ dbCount: number;
476
+ outputCount: number;
477
+ exactMatches: number;
478
+ contentMatches: number;
479
+ conflicts: number;
480
+ noMatches: number;
481
+ };
482
+ type MergeResult = {
483
+ messages: Message[];
484
+ stats: MergeStats;
485
+ conflicts?: Array<{
486
+ csvMsg: Message;
487
+ dbMsg: Message;
488
+ confidence: number;
489
+ }>;
490
+ warnings?: string[];
491
+ };
492
+ /**
493
+ * AC01 + AC02 + AC03 + AC04 + AC05: Main dedup and merge function
494
+ *
495
+ * Strategy:
496
+ * 1. Build GUID index for fast lookup
497
+ * 2. For each CSV message:
498
+ * a. Try exact GUID match (AC01)
499
+ * b. Try content equivalence (AC03)
500
+ * c. Apply DB authoritiveness if merging (AC02)
501
+ * d. Keep separate if no match
502
+ * 3. Add unmatched DB messages
503
+ * 4. Verify no data loss (AC04)
504
+ * 5. Ensure determinism (AC05)
505
+ */
506
+ declare function dedupAndMerge(csvMessages: Message[], dbMessages: Message[]): MergeResult;
507
+ type IngestOptions = {
508
+ attachmentRoots: string[];
509
+ messageDate?: string;
510
+ };
511
+ type CSVRow = {
512
+ [key: string]: string | undefined;
513
+ };
514
+ /**
515
+ * Main entry point: Ingest CSV file and convert to unified Message schema
516
+ */
517
+ declare function ingestCSV(csvFilePath: string, options: IngestOptions): Message[];
518
+ /**
519
+ * Export envelope wrapper for CSV ingestion output
520
+ */
521
+ declare function createExportEnvelope(messages: Message[]): ExportEnvelope;
522
+ type EnrichmentStats = {
523
+ processedCount: number;
524
+ failedCount: number;
525
+ startTime: string;
526
+ endTime: string;
527
+ };
528
+ type PipelineConfig = {
529
+ configHash: string;
530
+ };
531
+ /**
532
+ * Complete state for incremental enrichment tracking
533
+ * Stored in .imessage-state.json at output directory root
534
+ */
535
+ type IncrementalState = {
536
+ /** Schema version for backward compatibility */
537
+ version: string;
538
+ /** ISO 8601 UTC timestamp of last enrichment run */
539
+ lastEnrichedAt: string;
540
+ /** Total messages as of last run (for progress reporting) */
541
+ totalMessages: number;
542
+ /** Array of enriched message GUIDs (for delta detection) */
543
+ enrichedGuids: string[];
544
+ /** Pipeline configuration hash (detects config changes) */
545
+ pipelineConfig: PipelineConfig;
546
+ /** Optional: Stats from last enrichment run */
547
+ enrichmentStats: EnrichmentStats | null;
548
+ };
549
+ /**
550
+ * Result of delta detection
551
+ *
552
+ * Provides:
553
+ * - newGuids: Message GUIDs to enrich
554
+ * - totalMessages: Total current messages
555
+ * - previousEnrichedCount: Messages enriched in prior run
556
+ * - newCount: Number of new messages found
557
+ * - isFirstRun: Whether state file was missing
558
+ * - state: Loaded or created state for update after enrichment
559
+ */
560
+ type DeltaResult = {
561
+ /** GUIDs of messages to enrich (new since last run) */
562
+ newGuids: string[];
563
+ /** Total messages in normalized dataset */
564
+ totalMessages: number;
565
+ /** Count of previously enriched messages */
566
+ previousEnrichedCount: number;
567
+ /** Number of new messages found in delta */
568
+ newCount: number;
569
+ /** true if no prior state file found */
570
+ isFirstRun: boolean;
571
+ /** IncrementalState to update and save after enrichment */
572
+ state: IncrementalState;
573
+ };
574
+ /**
575
+ * AC02 + AC03: Extract message GUIDs from normalized messages
576
+ *
577
+ * Converts Message[] to Set<guid> for efficient delta computation
578
+ *
579
+ * @param messages - Normalized messages to process
580
+ * @returns Set of unique message GUIDs
581
+ */
582
+ declare function extractGuidsFromMessages(messages: Message[]): Set<string>;
583
+ /**
584
+ * AC05: Log human-readable delta summary
585
+ *
586
+ * Outputs lines like:
587
+ * - "Found 142 new messages to enrich"
588
+ * - "Previously enriched: 358"
589
+ * - "This is your first enrichment run"
590
+ * - "Delta: 28.3% of 500 total messages"
591
+ *
592
+ * @param result - DeltaResult to summarize
593
+ */
594
+ declare function logDeltaSummary(result: DeltaResult): void;
595
+ /**
596
+ * AC01-AC05: Perform complete delta detection
597
+ *
598
+ * Workflow:
599
+ * 1. AC04: Load previous state (null if missing)
600
+ * 2. AC02: Extract current message GUIDs
601
+ * 3. AC03: Compute delta (new GUIDs only)
602
+ * 4. AC05: Log summary
603
+ *
604
+ * Returns DeltaResult with:
605
+ * - newGuids to enrich
606
+ * - metadata for progress tracking
607
+ * - state to update after enrichment
608
+ *
609
+ * @param messages - Normalized messages from normalize-link stage
610
+ * @param stateFilePath - Path to .imessage-state.json
611
+ * @returns DeltaResult with new GUIDs and metadata
612
+ * @throws Error if state file can't be read (permission denied, etc.)
613
+ */
614
+ declare function detectDelta(messages: Message[], stateFilePath: string): Promise<DeltaResult>;
615
+ /**
616
+ * Calculate delta statistics for progress monitoring
617
+ *
618
+ * Useful for:
619
+ * - Progress bars (show percentage to enrich)
620
+ * - ETA calculation
621
+ * - Summary reports
622
+ *
623
+ * @param result - DeltaResult to analyze
624
+ * @returns Statistics object with counts and percentages
625
+ */
626
+ declare function getDeltaStats(result: DeltaResult): {
627
+ total: number;
628
+ new: number;
629
+ previous: number;
630
+ percentNew: number;
631
+ percentPrevious: number;
632
+ };
633
+ /**
634
+ * Options for merge behavior
635
+ */
636
+ type MergeOptions = {
637
+ /** Force refresh overwrites existing enrichments (default: false) */
638
+ forceRefresh?: boolean;
639
+ };
640
+ /**
641
+ * Statistics from merge operation
642
+ */
643
+ type MergeStatistics = {
644
+ /** Number of messages merged (existing GUIDs updated) */
645
+ mergedCount: number;
646
+ /** Number of messages added (new GUIDs) */
647
+ addedCount: number;
648
+ /** Number of messages with preserved enrichments */
649
+ preservedCount: number;
650
+ /** Total messages in result */
651
+ totalMessages: number;
652
+ /** Percentage of messages that were merged */
653
+ mergedPercentage?: number;
654
+ /** Percentage of messages that were added */
655
+ addedPercentage?: number;
656
+ };
657
+ /**
658
+ * Result of merge operation
659
+ */
660
+ type MergeResult2 = {
661
+ /** Merged messages with enrichments preserved/updated */
662
+ messages: Message[];
663
+ /** Statistics about the merge operation */
664
+ statistics: MergeStatistics;
665
+ /** Count of messages merged */
666
+ mergedCount: number;
667
+ /** Count of messages added */
668
+ addedCount: number;
669
+ /** Count of messages with preserved enrichments */
670
+ preservedCount: number;
671
+ };
672
+ /**
673
+ * AC02 + AC03: Merge new enrichments with existing messages
674
+ *
675
+ * Strategy:
676
+ * 1. Build index of existing messages by GUID
677
+ * 2. For each new message:
678
+ * - If GUID exists: merge enrichments (preserve existing unless --force-refresh)
679
+ * - If GUID is new: add message
680
+ * 3. Preserve enrichment order and structure
681
+ *
682
+ * @param existingMessages - Messages from prior enriched.json
683
+ * @param newMessages - New/updated messages from enrichment
684
+ * @param options - Merge options (forceRefresh, etc.)
685
+ * @returns MergeResult with merged messages and statistics
686
+ */
687
+ declare function mergeEnrichments(existingMessages: Message[], newMessages: Message[], options?: MergeOptions): MergeResult2;
688
+ export { validateGeneratedConfig, validateConfigSafe, validateConfig2 as validateConfig, substituteEnvVars, mergeEnrichments, mergeConfig, logDeltaSummary, loadConfigFile, loadConfig, isRetryableStatus, isConfigCached, is5xx, ingestCSV, getDeltaStats, getDefaultConfigPath, generateConfigContent, extractGuidsFromMessages, discoverConfigFile, detectDelta, detectConfigFormat, dedupAndMerge, createRateLimiter, createExportEnvelope, clearConfigCache, TapbackInfoSchema, TapbackInfo, ReplyInfoSchema, ReplyInfo, RateLimiter, RateLimitState, RateLimitConfig, MessageGUID, MessageCoreSchema, MessageCore, Message, MergeStats, MergeStatistics, MergeOptions, MediaProvenanceSchema, MediaProvenance, MediaMetaSchema, MediaMeta, MediaKind, MediaEnrichmentSchema, MediaEnrichment, IngestOptions, MergeResult as IngestMergeResult, ExportEnvelope, MergeResult2 as EnrichmentMergeResult, DeltaResult, DEFAULT_CONFIG, ConfigFormat, Config, ChatId, CSVRow, CONFIG_FILE_PATTERNS, ApiResponse };