@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.
- package/CHANGELOG.md +12 -0
- package/dist/bin/index.js +5121 -0
- package/dist/index.d.ts +688 -0
- package/dist/index.js +1729 -0
- package/package.json +1 -1
package/dist/index.d.ts
ADDED
|
@@ -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 };
|