@nathanvale/chatline 0.0.1

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 (216) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1535 -0
  4. package/dist/bin/index.js +5121 -0
  5. package/dist/cli/commands/clean.d.ts +17 -0
  6. package/dist/cli/commands/clean.d.ts.map +1 -0
  7. package/dist/cli/commands/clean.js +142 -0
  8. package/dist/cli/commands/clean.js.map +1 -0
  9. package/dist/cli/commands/doctor.d.ts +17 -0
  10. package/dist/cli/commands/doctor.d.ts.map +1 -0
  11. package/dist/cli/commands/doctor.js +202 -0
  12. package/dist/cli/commands/doctor.js.map +1 -0
  13. package/dist/cli/commands/enrich-ai.d.ts +17 -0
  14. package/dist/cli/commands/enrich-ai.d.ts.map +1 -0
  15. package/dist/cli/commands/enrich-ai.js +371 -0
  16. package/dist/cli/commands/enrich-ai.js.map +1 -0
  17. package/dist/cli/commands/index.d.ts +16 -0
  18. package/dist/cli/commands/index.d.ts.map +1 -0
  19. package/dist/cli/commands/index.js +16 -0
  20. package/dist/cli/commands/index.js.map +1 -0
  21. package/dist/cli/commands/ingest-csv.d.ts +17 -0
  22. package/dist/cli/commands/ingest-csv.d.ts.map +1 -0
  23. package/dist/cli/commands/ingest-csv.js +138 -0
  24. package/dist/cli/commands/ingest-csv.js.map +1 -0
  25. package/dist/cli/commands/ingest-db.d.ts +17 -0
  26. package/dist/cli/commands/ingest-db.d.ts.map +1 -0
  27. package/dist/cli/commands/ingest-db.js +159 -0
  28. package/dist/cli/commands/ingest-db.js.map +1 -0
  29. package/dist/cli/commands/init.d.ts +17 -0
  30. package/dist/cli/commands/init.d.ts.map +1 -0
  31. package/dist/cli/commands/init.js +110 -0
  32. package/dist/cli/commands/init.js.map +1 -0
  33. package/dist/cli/commands/normalize-link.d.ts +16 -0
  34. package/dist/cli/commands/normalize-link.d.ts.map +1 -0
  35. package/dist/cli/commands/normalize-link.js +144 -0
  36. package/dist/cli/commands/normalize-link.js.map +1 -0
  37. package/dist/cli/commands/render-markdown.d.ts +17 -0
  38. package/dist/cli/commands/render-markdown.d.ts.map +1 -0
  39. package/dist/cli/commands/render-markdown.js +218 -0
  40. package/dist/cli/commands/render-markdown.js.map +1 -0
  41. package/dist/cli/commands/stats.d.ts +17 -0
  42. package/dist/cli/commands/stats.d.ts.map +1 -0
  43. package/dist/cli/commands/stats.js +175 -0
  44. package/dist/cli/commands/stats.js.map +1 -0
  45. package/dist/cli/commands/validate.d.ts +17 -0
  46. package/dist/cli/commands/validate.d.ts.map +1 -0
  47. package/dist/cli/commands/validate.js +152 -0
  48. package/dist/cli/commands/validate.js.map +1 -0
  49. package/dist/cli/index.d.ts +13 -0
  50. package/dist/cli/index.d.ts.map +1 -0
  51. package/dist/cli/index.js +121 -0
  52. package/dist/cli/index.js.map +1 -0
  53. package/dist/cli/types.d.ts +93 -0
  54. package/dist/cli/types.d.ts.map +1 -0
  55. package/dist/cli/types.js +7 -0
  56. package/dist/cli/types.js.map +1 -0
  57. package/dist/cli/utils.d.ts +29 -0
  58. package/dist/cli/utils.d.ts.map +1 -0
  59. package/dist/cli/utils.js +53 -0
  60. package/dist/cli/utils.js.map +1 -0
  61. package/dist/cli.d.ts +9 -0
  62. package/dist/cli.d.ts.map +1 -0
  63. package/dist/cli.js +1805 -0
  64. package/dist/config/generator.d.ts +90 -0
  65. package/dist/config/generator.d.ts.map +1 -0
  66. package/dist/config/generator.js +320 -0
  67. package/dist/config/generator.js.map +1 -0
  68. package/dist/config/loader.d.ts +107 -0
  69. package/dist/config/loader.d.ts.map +1 -0
  70. package/dist/config/loader.js +251 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/schema.d.ts +107 -0
  73. package/dist/config/schema.d.ts.map +1 -0
  74. package/dist/config/schema.js +169 -0
  75. package/dist/config/schema.js.map +1 -0
  76. package/dist/enrich/audio-transcription.d.ts +77 -0
  77. package/dist/enrich/audio-transcription.d.ts.map +1 -0
  78. package/dist/enrich/audio-transcription.js +370 -0
  79. package/dist/enrich/audio-transcription.js.map +1 -0
  80. package/dist/enrich/checkpoint.d.ts +137 -0
  81. package/dist/enrich/checkpoint.d.ts.map +1 -0
  82. package/dist/enrich/checkpoint.js +205 -0
  83. package/dist/enrich/checkpoint.js.map +1 -0
  84. package/dist/enrich/idempotency.d.ts +90 -0
  85. package/dist/enrich/idempotency.d.ts.map +1 -0
  86. package/dist/enrich/idempotency.js +188 -0
  87. package/dist/enrich/idempotency.js.map +1 -0
  88. package/dist/enrich/image-analysis.d.ts +62 -0
  89. package/dist/enrich/image-analysis.d.ts.map +1 -0
  90. package/dist/enrich/image-analysis.js +264 -0
  91. package/dist/enrich/image-analysis.js.map +1 -0
  92. package/dist/enrich/index.d.ts +60 -0
  93. package/dist/enrich/index.d.ts.map +1 -0
  94. package/dist/enrich/index.js +74 -0
  95. package/dist/enrich/index.js.map +1 -0
  96. package/dist/enrich/link-enrichment.d.ts +37 -0
  97. package/dist/enrich/link-enrichment.d.ts.map +1 -0
  98. package/dist/enrich/link-enrichment.js +202 -0
  99. package/dist/enrich/link-enrichment.js.map +1 -0
  100. package/dist/enrich/pdf-video-handling.d.ts +49 -0
  101. package/dist/enrich/pdf-video-handling.d.ts.map +1 -0
  102. package/dist/enrich/pdf-video-handling.js +325 -0
  103. package/dist/enrich/pdf-video-handling.js.map +1 -0
  104. package/dist/enrich/progress-tracker.d.ts +120 -0
  105. package/dist/enrich/progress-tracker.d.ts.map +1 -0
  106. package/dist/enrich/progress-tracker.js +220 -0
  107. package/dist/enrich/progress-tracker.js.map +1 -0
  108. package/dist/enrich/providers/firecrawl.d.ts +18 -0
  109. package/dist/enrich/providers/firecrawl.d.ts.map +1 -0
  110. package/dist/enrich/providers/firecrawl.js +48 -0
  111. package/dist/enrich/providers/firecrawl.js.map +1 -0
  112. package/dist/enrich/providers/generic.d.ts +16 -0
  113. package/dist/enrich/providers/generic.d.ts.map +1 -0
  114. package/dist/enrich/providers/generic.js +36 -0
  115. package/dist/enrich/providers/generic.js.map +1 -0
  116. package/dist/enrich/providers/index.d.ts +14 -0
  117. package/dist/enrich/providers/index.d.ts.map +1 -0
  118. package/dist/enrich/providers/index.js +13 -0
  119. package/dist/enrich/providers/index.js.map +1 -0
  120. package/dist/enrich/providers/instagram.d.ts +16 -0
  121. package/dist/enrich/providers/instagram.d.ts.map +1 -0
  122. package/dist/enrich/providers/instagram.js +43 -0
  123. package/dist/enrich/providers/instagram.js.map +1 -0
  124. package/dist/enrich/providers/spotify.d.ts +16 -0
  125. package/dist/enrich/providers/spotify.d.ts.map +1 -0
  126. package/dist/enrich/providers/spotify.js +45 -0
  127. package/dist/enrich/providers/spotify.js.map +1 -0
  128. package/dist/enrich/providers/twitter.d.ts +16 -0
  129. package/dist/enrich/providers/twitter.d.ts.map +1 -0
  130. package/dist/enrich/providers/twitter.js +43 -0
  131. package/dist/enrich/providers/twitter.js.map +1 -0
  132. package/dist/enrich/providers/types.d.ts +47 -0
  133. package/dist/enrich/providers/types.d.ts.map +1 -0
  134. package/dist/enrich/providers/types.js +15 -0
  135. package/dist/enrich/providers/types.js.map +1 -0
  136. package/dist/enrich/providers/youtube.d.ts +16 -0
  137. package/dist/enrich/providers/youtube.d.ts.map +1 -0
  138. package/dist/enrich/providers/youtube.js +43 -0
  139. package/dist/enrich/providers/youtube.js.map +1 -0
  140. package/dist/enrich/rate-limiting.d.ts +118 -0
  141. package/dist/enrich/rate-limiting.d.ts.map +1 -0
  142. package/dist/enrich/rate-limiting.js +258 -0
  143. package/dist/enrich/rate-limiting.js.map +1 -0
  144. package/dist/index.d.ts +688 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +1729 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/ingest/dedup-merge.d.ts +82 -0
  149. package/dist/ingest/dedup-merge.d.ts.map +1 -0
  150. package/dist/ingest/dedup-merge.js +262 -0
  151. package/dist/ingest/dedup-merge.js.map +1 -0
  152. package/dist/ingest/ingest-csv.d.ts +62 -0
  153. package/dist/ingest/ingest-csv.d.ts.map +1 -0
  154. package/dist/ingest/ingest-csv.js +300 -0
  155. package/dist/ingest/ingest-csv.js.map +1 -0
  156. package/dist/ingest/ingest-db.d.ts +64 -0
  157. package/dist/ingest/ingest-db.d.ts.map +1 -0
  158. package/dist/ingest/ingest-db.js +172 -0
  159. package/dist/ingest/ingest-db.js.map +1 -0
  160. package/dist/ingest/link-replies-and-tapbacks.d.ts +53 -0
  161. package/dist/ingest/link-replies-and-tapbacks.d.ts.map +1 -0
  162. package/dist/ingest/link-replies-and-tapbacks.js +381 -0
  163. package/dist/ingest/link-replies-and-tapbacks.js.map +1 -0
  164. package/dist/normalize/date-converters.d.ts +45 -0
  165. package/dist/normalize/date-converters.d.ts.map +1 -0
  166. package/dist/normalize/date-converters.js +166 -0
  167. package/dist/normalize/date-converters.js.map +1 -0
  168. package/dist/normalize/path-validator.d.ts +65 -0
  169. package/dist/normalize/path-validator.d.ts.map +1 -0
  170. package/dist/normalize/path-validator.js +221 -0
  171. package/dist/normalize/path-validator.js.map +1 -0
  172. package/dist/normalize/validate-normalized.d.ts +45 -0
  173. package/dist/normalize/validate-normalized.d.ts.map +1 -0
  174. package/dist/normalize/validate-normalized.js +144 -0
  175. package/dist/normalize/validate-normalized.js.map +1 -0
  176. package/dist/render/embeds-blockquotes.d.ts +84 -0
  177. package/dist/render/embeds-blockquotes.d.ts.map +1 -0
  178. package/dist/render/embeds-blockquotes.js +204 -0
  179. package/dist/render/embeds-blockquotes.js.map +1 -0
  180. package/dist/render/grouping.d.ts +78 -0
  181. package/dist/render/grouping.d.ts.map +1 -0
  182. package/dist/render/grouping.js +134 -0
  183. package/dist/render/grouping.js.map +1 -0
  184. package/dist/render/index.d.ts +47 -0
  185. package/dist/render/index.d.ts.map +1 -0
  186. package/dist/render/index.js +245 -0
  187. package/dist/render/index.js.map +1 -0
  188. package/dist/render/reply-rendering.d.ts +88 -0
  189. package/dist/render/reply-rendering.d.ts.map +1 -0
  190. package/dist/render/reply-rendering.js +196 -0
  191. package/dist/render/reply-rendering.js.map +1 -0
  192. package/dist/schema/message.d.ts +125 -0
  193. package/dist/schema/message.d.ts.map +1 -0
  194. package/dist/schema/message.js +331 -0
  195. package/dist/schema/message.js.map +1 -0
  196. package/dist/utils/delta-detection.d.ts +107 -0
  197. package/dist/utils/delta-detection.d.ts.map +1 -0
  198. package/dist/utils/delta-detection.js +199 -0
  199. package/dist/utils/delta-detection.js.map +1 -0
  200. package/dist/utils/enrichment-merge.d.ts +135 -0
  201. package/dist/utils/enrichment-merge.d.ts.map +1 -0
  202. package/dist/utils/enrichment-merge.js +280 -0
  203. package/dist/utils/enrichment-merge.js.map +1 -0
  204. package/dist/utils/human.d.ts +15 -0
  205. package/dist/utils/human.d.ts.map +1 -0
  206. package/dist/utils/human.js +27 -0
  207. package/dist/utils/human.js.map +1 -0
  208. package/dist/utils/incremental-state.d.ts +133 -0
  209. package/dist/utils/incremental-state.d.ts.map +1 -0
  210. package/dist/utils/incremental-state.js +237 -0
  211. package/dist/utils/incremental-state.js.map +1 -0
  212. package/dist/utils/logger.d.ts +40 -0
  213. package/dist/utils/logger.d.ts.map +1 -0
  214. package/dist/utils/logger.js +176 -0
  215. package/dist/utils/logger.js.map +1 -0
  216. package/package.json +165 -0
@@ -0,0 +1,300 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { parse } from 'csv-parse/sync';
5
+ import { MessageSchema } from '../schema/message.js';
6
+ /**
7
+ * Main entry point: Ingest CSV file and convert to unified Message schema
8
+ */
9
+ export function ingestCSV(csvFilePath, options) {
10
+ const csvContent = readFileSync(csvFilePath, 'utf-8');
11
+ const rows = parse(csvContent, { columns: true });
12
+ const messages = [];
13
+ let lineNumber = 2; // Start at 2 (header is line 1)
14
+ for (const row of rows) {
15
+ const rowMessages = parseCSVRow(row, lineNumber, options);
16
+ messages.push(...rowMessages);
17
+ lineNumber++;
18
+ }
19
+ return messages;
20
+ }
21
+ /**
22
+ * Parse a single CSV row and produce 1-N messages
23
+ * Maps iMazing CSV format to unified Message schema
24
+ *
25
+ * AC01: Parse iMazing CSV rows with correct field mapping per CSV header
26
+ */
27
+ export function parseCSVRow(row, lineNumber, options) {
28
+ const messages = [];
29
+ // Extract iMazing CSV fields (headers have spaces, not underscores)
30
+ const messageDate = row['Message Date'];
31
+ const deliveredDate = row['Delivered Date'];
32
+ const readDate = row['Read Date'];
33
+ const editedDate = row['Edited Date'];
34
+ const service = row.Service;
35
+ const type = row.Type;
36
+ const senderName = row['Sender Name'];
37
+ const senderID = row['Sender ID'];
38
+ const status = row.Status;
39
+ const text = row.Text;
40
+ const subject = row.Subject;
41
+ const attachment = row.Attachment;
42
+ const attachmentType = row['Attachment type'];
43
+ const replyingTo = row['Replying to'];
44
+ // AC03: Convert CSV dates to ISO 8601 UTC with Z suffix
45
+ const date = convertToISO8601(messageDate || '');
46
+ if (!date)
47
+ return []; // Skip rows with invalid dates
48
+ // AC02: Split rows into text/media/tapback/notification by analyzing content
49
+ // Determine messageKind and isFromMe from Type field
50
+ const isFromMe = type === 'Outgoing' || type === 'Sent';
51
+ let messageKind = 'text';
52
+ if (type === 'Notification') {
53
+ messageKind = 'notification';
54
+ }
55
+ // Base message object with common fields
56
+ const baseMessage = {
57
+ isFromMe,
58
+ date,
59
+ };
60
+ // Conditionally add optional fields to satisfy exactOptionalPropertyTypes
61
+ const handle = senderName || senderID;
62
+ if (handle)
63
+ baseMessage.handle = handle;
64
+ if (service)
65
+ baseMessage.service = service;
66
+ if (subject)
67
+ baseMessage.subject = subject;
68
+ if (readDate)
69
+ baseMessage.dateRead = convertToISO8601(readDate);
70
+ if (deliveredDate)
71
+ baseMessage.dateDelivered = convertToISO8601(deliveredDate);
72
+ if (editedDate) {
73
+ baseMessage.dateEdited = convertToISO8601(editedDate);
74
+ baseMessage.isEdited = true;
75
+ }
76
+ if (status === 'Read')
77
+ baseMessage.isRead = true;
78
+ else if (status === 'Unread')
79
+ baseMessage.isRead = false;
80
+ // AC05: Preserve row metadata (source, line number) for provenance
81
+ const baseExportMetadata = {
82
+ source: 'csv',
83
+ lineNumber,
84
+ csvGuid: `csv:${lineNumber}:0`,
85
+ ...(replyingTo && { replyingTo }),
86
+ };
87
+ // Create text message
88
+ if (messageKind === 'text' && text) {
89
+ const textMessage = {
90
+ ...baseMessage,
91
+ guid: `csv:${lineNumber}:0`,
92
+ messageKind: 'text',
93
+ text,
94
+ exportMetadata: baseExportMetadata,
95
+ };
96
+ messages.push(textMessage);
97
+ }
98
+ // AC04: Resolve iMazing attachment paths to absolute paths when files exist
99
+ if (attachment && attachment.trim() !== '') {
100
+ const resolvedPath = resolveAttachmentPath({ filename: attachment }, {
101
+ ...options,
102
+ messageDate: date,
103
+ });
104
+ // Only create media message if path can be resolved to absolute path (schema requirement)
105
+ if (resolvedPath) {
106
+ const mediaMessage = {
107
+ ...baseMessage,
108
+ guid: `csv:${lineNumber}:0:media`,
109
+ messageKind: 'media',
110
+ media: {
111
+ id: `media:csv:${lineNumber}:0`,
112
+ filename: attachment,
113
+ path: resolvedPath,
114
+ mimeType: attachmentType || undefined,
115
+ mediaKind: inferMediaKind(attachmentType || ''),
116
+ },
117
+ exportMetadata: {
118
+ ...baseExportMetadata,
119
+ attachmentIndex: 0,
120
+ },
121
+ };
122
+ messages.push(mediaMessage);
123
+ }
124
+ }
125
+ // Create notification message if explicitly marked
126
+ if (messageKind === 'notification') {
127
+ const notificationMessage = {
128
+ ...baseMessage,
129
+ guid: `csv:${lineNumber}:0`,
130
+ messageKind: 'notification',
131
+ exportMetadata: baseExportMetadata,
132
+ };
133
+ messages.push(notificationMessage);
134
+ }
135
+ // Fallback: If no messages created but we have text, create text message
136
+ if (messages.length === 0 && text) {
137
+ const fallbackMessage = {
138
+ ...baseMessage,
139
+ guid: `csv:${lineNumber}:0`,
140
+ messageKind: 'text',
141
+ text,
142
+ exportMetadata: baseExportMetadata,
143
+ };
144
+ messages.push(fallbackMessage);
145
+ }
146
+ return messages;
147
+ }
148
+ /**
149
+ * Convert CSV date to ISO 8601 UTC with Z suffix
150
+ * Input format: "YYYY-MM-DD HH:MM:SS" (space-separated)
151
+ */
152
+ export function convertToISO8601(csvDate) {
153
+ if (!csvDate || csvDate.trim() === '') {
154
+ return null;
155
+ }
156
+ try {
157
+ // Normalize: convert space to T (CSV uses space, ISO 8601 uses T)
158
+ const normalized = csvDate.trim().replace(' ', 'T');
159
+ // Basic validation: should contain date separators
160
+ if (!normalized.includes('-') && !normalized.includes('/')) {
161
+ return null;
162
+ }
163
+ // Append Z if not present (assuming UTC)
164
+ let isoString = normalized;
165
+ if (!normalized.includes('Z') && !normalized.match(/[+-]\d{2}:/)) {
166
+ isoString = `${normalized}Z`;
167
+ }
168
+ // Parse and validate
169
+ const date = new Date(isoString);
170
+ if (Number.isNaN(date.getTime())) {
171
+ return null;
172
+ }
173
+ // Return ISO 8601 with Z suffix
174
+ return date.toISOString();
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ /**
181
+ * Resolve attachment path to absolute path when file exists
182
+ */
183
+ export function resolveAttachmentPath(attachment, options) {
184
+ if (!attachment)
185
+ return null;
186
+ const { attachmentRoots, messageDate } = options;
187
+ // If already absolute and exists, return it
188
+ if (attachment.copied_path?.startsWith('/')) {
189
+ if (existsSync(attachment.copied_path)) {
190
+ return attachment.copied_path;
191
+ }
192
+ }
193
+ // Expand tilde if present
194
+ if (attachment.copied_path?.startsWith('~')) {
195
+ const expanded = attachment.copied_path.replace('~', os.homedir());
196
+ if (existsSync(expanded)) {
197
+ return expanded;
198
+ }
199
+ }
200
+ // Search using timestamp pattern in attachment roots
201
+ if (messageDate && attachmentRoots.length > 0) {
202
+ const dateStr = formatDateForAttachmentSearch(messageDate);
203
+ const filename = attachment.filename || 'unknown';
204
+ const senderName = attachment.senderName || '*';
205
+ for (const root of attachmentRoots) {
206
+ // Try exact pattern: YYYY-MM-DD HH MM SS - SenderName - filename
207
+ const pattern = `${dateStr} - ${senderName} - ${filename}`;
208
+ const fullPath = path.join(root, pattern);
209
+ if (existsSync(fullPath)) {
210
+ return fullPath;
211
+ }
212
+ // Try wildcard pattern if sender name unknown
213
+ if (senderName === '*' && existsSync(root)) {
214
+ try {
215
+ const files = readdirSync(root).filter((f) => {
216
+ return f.includes(dateStr) && f.endsWith(filename);
217
+ });
218
+ if (files.length > 0 && files[0]) {
219
+ return path.join(root, files[0]);
220
+ }
221
+ }
222
+ catch {
223
+ // Directory doesn't exist or can't be read
224
+ }
225
+ }
226
+ }
227
+ }
228
+ // Not found
229
+ return null;
230
+ }
231
+ /**
232
+ * Infer media kind from MIME type
233
+ */
234
+ export function inferMediaKind(mimeType) {
235
+ if (!mimeType)
236
+ return 'unknown';
237
+ if (mimeType.startsWith('image/'))
238
+ return 'image';
239
+ if (mimeType.startsWith('audio/'))
240
+ return 'audio';
241
+ if (mimeType.startsWith('video/'))
242
+ return 'video';
243
+ if (mimeType.includes('pdf'))
244
+ return 'pdf';
245
+ return 'unknown';
246
+ }
247
+ /**
248
+ * Format ISO 8601 date for attachment search pattern
249
+ * Converts: 2023-10-23T06:52:57.000Z → 2023-10-23 06 52 57
250
+ */
251
+ export function formatDateForAttachmentSearch(isoDate) {
252
+ try {
253
+ const date = new Date(isoDate);
254
+ const year = date.getUTCFullYear();
255
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
256
+ const day = String(date.getUTCDate()).padStart(2, '0');
257
+ const hours = String(date.getUTCHours()).padStart(2, '0');
258
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
259
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
260
+ return `${year}-${month}-${day} ${hours} ${minutes} ${seconds}`;
261
+ }
262
+ catch {
263
+ return '';
264
+ }
265
+ }
266
+ /**
267
+ * Export envelope wrapper for CSV ingestion output
268
+ */
269
+ export function createExportEnvelope(messages) {
270
+ return {
271
+ schemaVersion: '2.0.0',
272
+ source: 'csv',
273
+ createdAt: new Date().toISOString(),
274
+ messages,
275
+ };
276
+ }
277
+ /**
278
+ * Validate all messages pass schema validation
279
+ */
280
+ export function validateMessages(messages) {
281
+ const errors = [];
282
+ for (let i = 0; i < messages.length; i++) {
283
+ const message = messages[i];
284
+ if (!message)
285
+ continue;
286
+ const result = MessageSchema.safeParse(message);
287
+ if (!result.success) {
288
+ errors.push({
289
+ index: i,
290
+ message,
291
+ issues: result.error.issues,
292
+ });
293
+ }
294
+ }
295
+ return {
296
+ valid: errors.length === 0,
297
+ errors,
298
+ };
299
+ }
300
+ //# sourceMappingURL=ingest-csv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-csv.js","sourceRoot":"","sources":["../../src/ingest/ingest-csv.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import type { ExportEnvelope, Message } from '../schema/message.js';
2
+ export type DBMessage = {
3
+ guid: string;
4
+ rowid?: number;
5
+ text?: string | null;
6
+ is_from_me: number;
7
+ date: number;
8
+ chat_id?: string;
9
+ handle?: string;
10
+ service?: string;
11
+ subject?: string | null;
12
+ attachments?: DBAttachment[];
13
+ [key: string]: unknown;
14
+ };
15
+ export type DBAttachment = {
16
+ id: string;
17
+ filename: string;
18
+ mime_type?: string;
19
+ uti?: string | null;
20
+ copied_path?: string;
21
+ total_bytes?: number;
22
+ [key: string]: unknown;
23
+ };
24
+ export type IngestOptions = {
25
+ attachmentRoots: string[];
26
+ };
27
+ /**
28
+ * Split a single DB message into multiple Message objects
29
+ * - 1 text message (if text exists)
30
+ * - N media messages (one per attachment)
31
+ * All parts share same groupGuid (original DB guid) and timestamps
32
+ */
33
+ export declare function splitDBMessage(dbMessage: DBMessage, lineNumber: number, _options: IngestOptions): Message[];
34
+ /**
35
+ * Generate stable part GUID using format: p:<index>/<original_guid>
36
+ * This ensures:
37
+ * - Deterministic generation (same input → same output)
38
+ * - Stable ordering (index reflects order in split)
39
+ * - Uniqueness within a message's parts
40
+ */
41
+ export declare function generatePartGUID(originalGuid: string, index: number): string;
42
+ /**
43
+ * Convert Apple epoch timestamp to ISO 8601 UTC with Z suffix
44
+ * Apple epoch = seconds since 2001-01-01 00:00:00 UTC
45
+ *
46
+ * Handles:
47
+ * - Seconds precision: 718110777
48
+ * - Milliseconds precision: 718110777000
49
+ * - Nanoseconds precision: 718110777123456789 (truncate to seconds)
50
+ */
51
+ export declare function convertAppleEpochToISO8601(appleEpoch: number): string | null;
52
+ /**
53
+ * Infer media kind from MIME type
54
+ */
55
+ export declare function inferMediaKind(mimeType: string): 'image' | 'audio' | 'video' | 'pdf' | 'unknown';
56
+ /**
57
+ * Main entry point: Ingest DB messages and split into normalized schema
58
+ */
59
+ export declare function ingestDBMessages(dbMessages: DBMessage[], options: IngestOptions): Message[];
60
+ /**
61
+ * Create export envelope for DB ingestion output
62
+ */
63
+ export declare function createExportEnvelope(messages: Message[]): ExportEnvelope;
64
+ //# sourceMappingURL=ingest-db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-db.d.ts","sourceRoot":"","sources":["../../src/ingest/ingest-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAEnE,MAAM,MAAM,SAAS,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,YAAY,EAAE,CAAA;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,eAAe,EAAE,MAAM,EAAE,CAAA;CACzB,CAAA;AASD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,aAAa,GACrB,OAAO,EAAE,CAyEX;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6B5E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC7B,QAAQ,EAAE,MAAM,GACd,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,CASjD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC/B,UAAU,EAAE,SAAS,EAAE,EACvB,OAAO,EAAE,aAAa,GACpB,OAAO,EAAE,CAWX;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,cAAc,CAOxE"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Apple epoch reference: seconds since 2001-01-01 00:00:00 UTC
3
+ * Unix epoch reference: seconds since 1970-01-01 00:00:00 UTC
4
+ * Difference: 31 years = 978307200 seconds
5
+ */
6
+ const APPLE_EPOCH_OFFSET = 978307200;
7
+ /**
8
+ * Split a single DB message into multiple Message objects
9
+ * - 1 text message (if text exists)
10
+ * - N media messages (one per attachment)
11
+ * All parts share same groupGuid (original DB guid) and timestamps
12
+ */
13
+ export function splitDBMessage(dbMessage, lineNumber, _options) {
14
+ const messages = [];
15
+ const originalGuid = dbMessage.guid;
16
+ const attachments = dbMessage.attachments || [];
17
+ const date = convertAppleEpochToISO8601(dbMessage.date);
18
+ const isFromMe = dbMessage.is_from_me === 1;
19
+ if (!date)
20
+ return []; // Skip invalid dates
21
+ // Common fields for all parts
22
+ const baseMessage = {
23
+ isFromMe,
24
+ date,
25
+ groupGuid: originalGuid,
26
+ };
27
+ // Conditionally add optional fields to satisfy exactOptionalPropertyTypes
28
+ if (dbMessage.handle)
29
+ baseMessage.handle = dbMessage.handle;
30
+ if (dbMessage.chat_id)
31
+ baseMessage.chatId = dbMessage.chat_id;
32
+ if (dbMessage.service)
33
+ baseMessage.service = dbMessage.service;
34
+ if (dbMessage.subject)
35
+ baseMessage.subject = dbMessage.subject;
36
+ // Part index counter (0 = text, 1+ = media)
37
+ let partIndex = 0;
38
+ // 1. Create text message if text exists
39
+ if (dbMessage.text) {
40
+ const textMessage = {
41
+ ...baseMessage,
42
+ guid: generatePartGUID(originalGuid, partIndex),
43
+ messageKind: 'text',
44
+ text: dbMessage.text,
45
+ exportMetadata: {
46
+ source: 'db',
47
+ lineNumber,
48
+ parentGUID: originalGuid,
49
+ partIndex,
50
+ },
51
+ };
52
+ messages.push(textMessage);
53
+ partIndex++;
54
+ }
55
+ // 2. Create media messages for each attachment
56
+ attachments.forEach((att, attachmentIndex) => {
57
+ const mediaMessage = {
58
+ ...baseMessage,
59
+ guid: generatePartGUID(originalGuid, partIndex),
60
+ messageKind: 'media',
61
+ media: {
62
+ id: att.id || `media:${originalGuid}:${attachmentIndex}`,
63
+ filename: att.filename || 'unknown',
64
+ path: att.copied_path || null,
65
+ mimeType: att.mime_type || undefined,
66
+ uti: att.uti || undefined,
67
+ size: att.total_bytes || undefined,
68
+ mediaKind: inferMediaKind(att.mime_type || ''),
69
+ },
70
+ exportMetadata: {
71
+ source: 'db',
72
+ lineNumber,
73
+ parentGUID: originalGuid,
74
+ partIndex,
75
+ attachmentIndex,
76
+ },
77
+ };
78
+ messages.push(mediaMessage);
79
+ partIndex++;
80
+ });
81
+ return messages;
82
+ }
83
+ /**
84
+ * Generate stable part GUID using format: p:<index>/<original_guid>
85
+ * This ensures:
86
+ * - Deterministic generation (same input → same output)
87
+ * - Stable ordering (index reflects order in split)
88
+ * - Uniqueness within a message's parts
89
+ */
90
+ export function generatePartGUID(originalGuid, index) {
91
+ return `p:${index}/${originalGuid}`;
92
+ }
93
+ /**
94
+ * Convert Apple epoch timestamp to ISO 8601 UTC with Z suffix
95
+ * Apple epoch = seconds since 2001-01-01 00:00:00 UTC
96
+ *
97
+ * Handles:
98
+ * - Seconds precision: 718110777
99
+ * - Milliseconds precision: 718110777000
100
+ * - Nanoseconds precision: 718110777123456789 (truncate to seconds)
101
+ */
102
+ export function convertAppleEpochToISO8601(appleEpoch) {
103
+ try {
104
+ // Determine if input is in seconds, milliseconds, or nanoseconds
105
+ // Apple epoch realistic ranges:
106
+ // - Seconds: 0 to ~5,000,000,000 (2001 to ~2159)
107
+ // - Milliseconds: 0 to ~5,000,000,000,000 (2001 to ~2159)
108
+ // - Nanoseconds: anything > 1e15
109
+ let seconds;
110
+ if (appleEpoch > 1000000000000000) {
111
+ // Clearly nanoseconds (> 1 quadrillion)
112
+ seconds = Math.floor(appleEpoch / 1000000000);
113
+ }
114
+ else if (appleEpoch > 100000000000) {
115
+ // Likely milliseconds (> 100 billion, beyond realistic seconds range)
116
+ seconds = Math.floor(appleEpoch / 1000);
117
+ }
118
+ else {
119
+ // Seconds (includes values up to ~5 billion, covering years 2001-2159)
120
+ seconds = appleEpoch;
121
+ }
122
+ // Convert Apple epoch to Unix epoch
123
+ const unixSeconds = seconds + APPLE_EPOCH_OFFSET;
124
+ // Create Date and convert to ISO 8601
125
+ const date = new Date(unixSeconds * 1000);
126
+ return date.toISOString();
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /**
133
+ * Infer media kind from MIME type
134
+ */
135
+ export function inferMediaKind(mimeType) {
136
+ if (!mimeType)
137
+ return 'unknown';
138
+ if (mimeType.startsWith('image/'))
139
+ return 'image';
140
+ if (mimeType.startsWith('audio/'))
141
+ return 'audio';
142
+ if (mimeType.startsWith('video/'))
143
+ return 'video';
144
+ if (mimeType.includes('pdf'))
145
+ return 'pdf';
146
+ return 'unknown';
147
+ }
148
+ /**
149
+ * Main entry point: Ingest DB messages and split into normalized schema
150
+ */
151
+ export function ingestDBMessages(dbMessages, options) {
152
+ const messages = [];
153
+ let lineNumber = 1;
154
+ for (const dbMsg of dbMessages) {
155
+ const splitMessages = splitDBMessage(dbMsg, lineNumber, options);
156
+ messages.push(...splitMessages);
157
+ lineNumber++;
158
+ }
159
+ return messages;
160
+ }
161
+ /**
162
+ * Create export envelope for DB ingestion output
163
+ */
164
+ export function createExportEnvelope(messages) {
165
+ return {
166
+ schemaVersion: '2.0.0',
167
+ source: 'db',
168
+ createdAt: new Date().toISOString(),
169
+ messages,
170
+ };
171
+ }
172
+ //# sourceMappingURL=ingest-db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-db.js","sourceRoot":"","sources":["../../src/ingest/ingest-db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { Message } from '#schema/message';
2
+ type ScoredCandidate = {
3
+ message: Message;
4
+ score: number;
5
+ reasons: string[];
6
+ };
7
+ type AmbiguousLink = {
8
+ messageGuid: string;
9
+ selectedTarget: string;
10
+ candidates: ScoredCandidate[];
11
+ tieCount: number;
12
+ confidenceScore: number;
13
+ };
14
+ type LinkingOptions = {
15
+ trackAmbiguous?: boolean;
16
+ minConfidenceThreshold?: number;
17
+ };
18
+ type LinkingResult = {
19
+ messages: Message[];
20
+ ambiguousLinks?: AmbiguousLink[];
21
+ };
22
+ /**
23
+ * AC01 + AC02: Link replies to their parent messages
24
+ *
25
+ * Primary: DB association_guid when present
26
+ * Fallback: Heuristics using timestamp and content matching
27
+ */
28
+ export declare function linkRepliesToParents(messages: Message[], options?: LinkingOptions): Message[] | LinkingResult;
29
+ /**
30
+ * AC03: Link tapbacks to their parent messages
31
+ *
32
+ * Primary: DB association_guid when present
33
+ * Fallback: Heuristics preferring media messages
34
+ */
35
+ export declare function linkTapbacksToParents(messages: Message[], options?: LinkingOptions): Message[] | LinkingResult;
36
+ /**
37
+ * AC04: Detect and report ambiguous links with confidence scores
38
+ */
39
+ export declare function detectAmbiguousLinks(messages: Message[]): {
40
+ tieCount: number;
41
+ ambiguousMessages: {
42
+ messageGuid: string;
43
+ selectedTarget: string;
44
+ tieCount: number;
45
+ topCandidates: {
46
+ guid: string;
47
+ score: number;
48
+ reasons: string[];
49
+ }[];
50
+ }[];
51
+ };
52
+ export type { LinkingResult, AmbiguousLink };
53
+ //# sourceMappingURL=link-replies-and-tapbacks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-replies-and-tapbacks.d.ts","sourceRoot":"","sources":["../../src/ingest/link-replies-and-tapbacks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAiB9C,KAAK,eAAe,GAAG;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AAED,KAAK,aAAa,GAAG;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,eAAe,EAAE,CAAA;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,KAAK,cAAc,GAAG;IACrB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED,KAAK,aAAa,GAAG;IACpB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,cAAc,CAAC,EAAE,aAAa,EAAE,CAAA;CAChC,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,GAAE,cAAmB,GAC1B,OAAO,EAAE,GAAG,aAAa,CAuF3B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACpC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,GAAE,cAAmB,GAC1B,OAAO,EAAE,GAAG,aAAa,CAkF3B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE;;;;;;;;;;;;EA0BvD;AA2PD,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,CAAA"}