imsg-mcp 1.2.0 → 1.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.
@@ -1 +0,0 @@
1
- {"version":3,"file":"imessage-db-BYU5mxM-.js","sources":["../src/native-bridge.ts","../src/parsers/buffer-reader.ts","../src/parsers/typedstream-parser.ts","../src/attributed-body-text.ts","../src/contacts-db.ts","../src/conversation-snippet.ts","../src/plist-text.ts","../src/slug-store.ts","../src/imessage-db.ts"],"sourcesContent":["/**\n * Native bridge — tries to load the Rust native module for accelerated\n * SQLite queries and blob parsing. Falls back to the TypeScript implementation\n * if the native module is unavailable.\n */\n\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport interface NativeModule {\n listConversations(\n dbPath: string,\n contactsMainPath: string,\n contactsSourcesDir: string | null,\n slugsDbPath: string,\n limit?: number | null,\n ): Promise<NativeConversation[]>;\n\n getMessages(\n dbPath: string,\n chatIdentifier: string,\n limit?: number | null,\n includeReactionDetails?: boolean | null,\n ): Promise<NativeMessage[]>;\n\n parseAttributedBody(blob: Buffer): string | null;\n\n resolveContacts(\n contactsMainPath: string,\n contactsSourcesDir: string | null,\n handles: string[],\n ): Promise<Record<string, string>>;\n}\n\nexport interface NativeConversation {\n chatId: string;\n chatIdentifier: string;\n displayName?: string;\n rawIdentifier: string;\n participants: string[];\n lastMessageDate?: number;\n lastMessageSnippet?: string;\n unreadCount: number;\n threadSlug: string;\n isGroupChat: boolean;\n serviceType: string;\n}\n\nexport interface NativeMessage {\n id: number;\n guid: string;\n text?: string;\n handle: string;\n displayName?: string;\n isFromMe: boolean;\n date: number;\n dateRead?: number;\n dateDelivered?: number;\n isRead: boolean;\n isDelivered: boolean;\n chatId: string;\n service: string;\n isReaction: boolean;\n isReply: boolean;\n replyToText?: string;\n replyToGuid?: string;\n hasAttachments: boolean;\n}\n\nlet _native: NativeModule | null | undefined;\n\n/**\n * Try to load the native module. Returns null if unavailable.\n * Result is cached after first attempt.\n *\n * Set `IMSG_DISABLE_NATIVE=1` to force the TypeScript fallback (useful for\n * testing the fallback path or comparing performance).\n */\nexport function tryLoadNative(): NativeModule | null {\n if (_native !== undefined) return _native;\n\n if (process.env.IMSG_DISABLE_NATIVE === \"1\") {\n _native = null;\n return null;\n }\n\n try {\n const require = createRequire(import.meta.url);\n // Try loading from the native/ directory relative to dist/\n const nativePath = join(__dirname, \"..\", \"native\", \"index.js\");\n _native = require(nativePath) as NativeModule;\n return _native;\n } catch {\n // Native module not available — fall back to TS\n _native = null;\n return null;\n }\n}\n\n/**\n * Check if the native module is available.\n */\nexport function hasNativeModule(): boolean {\n return tryLoadNative() !== null;\n}\n","/**\n * Buffer reader with cursor tracking for binary format parsing.\n * Forked from imessage-parser's BufferReader to remove the upstream dependency.\n */\nexport class BufferReader {\n private buffer: Buffer;\n private offset: number;\n\n constructor(buffer: Buffer, initialOffset = 0) {\n this.buffer = buffer;\n this.offset = initialOffset;\n }\n\n get position(): number {\n return this.offset;\n }\n\n get length(): number {\n return this.buffer.length;\n }\n\n get remaining(): number {\n return this.buffer.length - this.offset;\n }\n\n seek(position: number): void {\n if (position < 0 || position > this.buffer.length) {\n throw new Error(`Invalid seek position: ${position}`);\n }\n this.offset = position;\n }\n\n skip(bytes: number): void {\n this.offset += bytes;\n }\n\n readUInt8(): number {\n const value = this.buffer.readUInt8(this.offset);\n this.offset += 1;\n return value;\n }\n\n readUInt16LE(): number {\n const value = this.buffer.readUInt16LE(this.offset);\n this.offset += 2;\n return value;\n }\n\n readBytes(length: number): Buffer {\n if (this.offset + length > this.buffer.length) {\n throw new Error(\"Attempt to read beyond buffer length\");\n }\n const bytes = this.buffer.subarray(this.offset, this.offset + length);\n this.offset += length;\n return bytes;\n }\n\n readString(length: number, encoding: BufferEncoding = \"utf8\"): string {\n const bytes = this.readBytes(length);\n return bytes.toString(encoding);\n }\n\n findPattern(pattern: Buffer | string): number {\n const searchBuffer = typeof pattern === \"string\" ? Buffer.from(pattern) : pattern;\n const index = this.buffer.indexOf(searchBuffer, this.offset);\n return index >= 0 ? index : -1;\n }\n\n peekByte(): number | null {\n if (this.offset >= this.buffer.length) return null;\n return this.buffer[this.offset];\n }\n\n peekBytes(length: number): Buffer | null {\n if (this.offset + length > this.buffer.length) return null;\n return this.buffer.subarray(this.offset, this.offset + length);\n }\n}\n","/**\n * Parser for Apple's typedstream format used in NSArchiver.\n * Forked from imessage-parser's TypedStreamParser to remove the upstream dependency.\n *\n * This handles the binary `attributedBody` column in the iMessage database,\n * which stores NSAttributedString data serialised via NSArchiver (typedstream format).\n *\n * SAFETY: All loops have iteration guards to prevent infinite loops on malformed blobs.\n */\nimport { BufferReader } from \"./buffer-reader.js\";\n\nexport interface NSStringData {\n className: string;\n content: string;\n encoding: string;\n}\n\n/**\n * Preamble bytes following \"NSString\" before the length byte.\n * Apple uses variants of this 5-byte sequence — most commonly 0x94, but\n * 0x95 appears for messages that contain DataDetector annotations\n * (phone numbers, dates, auth codes). Both must be recognized or the\n * length byte will be misread (causing extracted text leaks like\n * \"()*+Z$classname...\").\n */\nconst PREAMBLE_BYTE_2_VARIANTS = new Set([0x94, 0x95]);\nconst PREAMBLE_LEN = 5;\n\nfunction matchesNSStringPreamble(bytes: Buffer | null): boolean {\n if (!bytes || bytes.length < PREAMBLE_LEN) return false;\n return (\n bytes[0] === 0x01 &&\n PREAMBLE_BYTE_2_VARIANTS.has(bytes[1]) &&\n bytes[2] === 0x84 &&\n bytes[3] === 0x01 &&\n bytes[4] === 0x2b\n );\n}\n\nconst METADATA_KEYWORDS = [\n \"streamtyped\",\n \"NSMutableAttributedString\",\n \"NSAttributedString\",\n \"NSObject\",\n \"NSMutableString\",\n \"NSString\",\n \"NSDictionary\",\n \"NSNumber\",\n \"NSValue\",\n \"NSFont\",\n \"NSParagraphStyle\",\n \"__kIM\",\n \"NSData\",\n \"bplist\",\n \"NSKeyedArchiver\",\n \"NS.rangeval\",\n \"Z$classname\",\n \"$class\",\n \"$classname\",\n];\n\nexport class TypedStreamParser {\n private reader: BufferReader;\n private headerParsed = false;\n\n constructor(buffer: Buffer) {\n this.reader = new BufferReader(buffer);\n }\n\n /** Parse all NSString objects found in the buffer. */\n parseAllNSStrings(): NSStringData[] {\n const strings: NSStringData[] = [];\n this.parseHeader();\n\n const maxIter = this.reader.length;\n let iter = 0;\n while (this.reader.remaining > 0 && iter++ < maxIter) {\n const posBefore = this.reader.position;\n const found = this.parseNSString();\n if (found) {\n strings.push(found);\n } else if (this.reader.remaining > 0) {\n // If parseNSString didn't advance, skip ahead\n if (this.reader.position <= posBefore) {\n this.reader.skip(1);\n }\n }\n // Stall detection: if position hasn't changed, force advance\n if (this.reader.position <= posBefore && this.reader.remaining > 0) {\n this.reader.skip(1);\n }\n }\n\n return strings;\n }\n\n /** Fallback: extract readable text segments by scanning for printable byte runs. */\n extractReadableText(): string[] {\n this.reader.seek(0);\n const texts: string[] = [];\n let current = \"\";\n let inText = false;\n\n const maxIter = this.reader.length;\n let iter = 0;\n while (this.reader.remaining > 0 && iter++ < maxIter) {\n const posBefore = this.reader.position;\n const byte = this.reader.readUInt8();\n\n if ((byte >= 32 && byte <= 126) || byte === 9 || byte === 10 || byte === 13) {\n // ASCII printable or whitespace\n current += String.fromCharCode(byte);\n inText = true;\n } else if (byte >= 0xc0 && byte <= 0xf7) {\n // Potential UTF-8 multi-byte sequence start\n const extraBytes = byte < 0xe0 ? 1 : byte < 0xf0 ? 2 : 3;\n if (this.reader.remaining >= extraBytes) {\n // Read continuation bytes\n const bytes = [byte];\n let valid = true;\n for (let i = 0; i < extraBytes; i++) {\n const cont = this.reader.readUInt8();\n if ((cont & 0xc0) !== 0x80) {\n valid = false;\n break;\n }\n bytes.push(cont);\n }\n if (valid) {\n try {\n current += Buffer.from(bytes).toString(\"utf8\");\n inText = true;\n } catch {\n this.flushSegment(current, inText, texts);\n current = \"\";\n inText = false;\n }\n } else {\n this.flushSegment(current, inText, texts);\n current = \"\";\n inText = false;\n }\n } else {\n // Not enough bytes for UTF-8 sequence\n this.flushSegment(current, inText, texts);\n current = \"\";\n inText = false;\n }\n } else {\n // Control char or invalid byte\n this.flushSegment(current, inText, texts);\n current = \"\";\n inText = false;\n }\n\n // Stall detection: position must have advanced\n if (this.reader.position <= posBefore) {\n if (this.reader.remaining > 0) this.reader.skip(1);\n else break;\n }\n }\n\n this.flushSegment(current, true, texts);\n return texts;\n }\n\n // ── Private ──────────────────────────────────────────────────────────\n\n private parseHeader(): void {\n if (this.headerParsed) return;\n const magic = this.reader.peekBytes(11);\n if (!magic || magic.toString(\"ascii\") !== \"streamtyped\") return;\n\n this.reader.skip(11);\n // Skip until we find an uppercase letter (class name start), with iteration guard\n const maxSkip = Math.min(this.reader.remaining, 256);\n let skipped = 0;\n while (this.reader.remaining > 0 && skipped++ < maxSkip) {\n const next4 = this.reader.peekBytes(4);\n if (next4 && /^[A-Z]/.test(next4.toString(\"ascii\"))) break;\n this.reader.skip(1);\n }\n this.headerParsed = true;\n }\n\n private parseNSString(): NSStringData | null {\n const pos = this.reader.findPattern(\"NSString\");\n if (pos === -1) {\n // No more NSString patterns — skip to end to stop the loop\n this.reader.seek(this.reader.length);\n return null;\n }\n\n this.reader.seek(pos + 8); // skip \"NSString\"\n\n const preamble = this.reader.peekBytes(PREAMBLE_LEN);\n if (matchesNSStringPreamble(preamble)) {\n this.reader.skip(PREAMBLE_LEN);\n }\n\n if (this.reader.remaining < 1) return null;\n const lengthByte = this.reader.readUInt8();\n let length: number;\n if (lengthByte === 0x81) {\n if (this.reader.remaining < 2) return null;\n length = this.reader.readUInt16LE();\n } else {\n length = lengthByte;\n }\n\n if (length === 0 || length > this.reader.remaining) return null;\n\n return {\n className: \"NSString\",\n content: this.reader.readString(length, \"utf8\"),\n encoding: \"utf8\",\n };\n }\n\n private flushSegment(text: string, inText: boolean, out: string[]): void {\n if (!inText || text.length <= 3) return;\n const cleaned = this.cleanText(text);\n if (cleaned.length > 3) out.push(cleaned);\n }\n\n private cleanText(text: string): string {\n let cleaned = text;\n for (const kw of METADATA_KEYWORDS) {\n cleaned = cleaned.replaceAll(kw, \"\");\n }\n return (\n cleaned\n .replace(/\\{[^}]*\\}/g, \"\")\n .replace(/\\[[^\\]]*\\]/g, \"\")\n // biome-ignore lint/suspicious/noControlCharactersInRegex: typedstream blobs contain binary control bytes.\n .replace(/[\\x00-\\x1f]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n }\n}\n","import { tryLoadNative } from \"./native-bridge.js\";\nimport { TypedStreamParser } from \"./parsers/typedstream-parser.js\";\n\n/** Cache the native lookup once — null means TS fallback path. */\nlet _nativeParseAttributedBody: ((blob: Buffer) => string | null) | null | undefined;\nfunction getNativeParser(): ((blob: Buffer) => string | null) | null {\n if (_nativeParseAttributedBody !== undefined) return _nativeParseAttributedBody;\n const native = tryLoadNative();\n _nativeParseAttributedBody = native ? native.parseAttributedBody.bind(native) : null;\n return _nativeParseAttributedBody;\n}\n\n/**\n * Decide whether to trust a native parser result. The Rust path uses simpler\n * heuristics and may return attachment IDs / metadata that the TS path knows\n * to filter. When we're unsure, fall through to TS for safety.\n */\nfunction nativeResultLooksTrustworthy(text: string): boolean {\n // Attachment GUID markers — never user-visible text\n if (/at_\\d+_[0-9A-F-]{8,}/i.test(text)) return false;\n // Apple internal attribute name markers\n if (/__kIM[A-Z]/.test(text)) return false;\n // Class / archiver metadata\n if (/\\$class|streamtyped|NSKeyedArchiver/.test(text)) return false;\n if (/^NS[A-Z][a-z]+/.test(text)) return false;\n // Doubled-uppercase-letter prefix — almost certainly a typedstream length-byte\n // leak (e.g. \"HHeres the question...\" where the byte 'H' = 0x48 = 72 is the\n // length of the actual content \"Heres the question...\"). The Rust path uses\n // simple split-on-control heuristics that don't understand typedstream framing,\n // so it can include the length byte as the first char of the result.\n // Reject so we fall through to the TS path's structured parser.\n if (/^([A-Z])\\1[a-z]/.test(text)) return false;\n return true;\n}\n\nconst METADATA_PATTERNS = [\n /^streamtyped$/i,\n /^NS[A-Z]/,\n /^[^A-Za-z0-9]*__kIM/,\n /^MessagePartAttributeName$/i,\n /^DataDetectedAttributeName$/i,\n /^CalendarEventAttributeName$/i,\n /^X\\$version/,\n /^WversionYdd-result/,\n /^bplist/,\n /^\\$class/,\n /^RMSV\\$class/,\n /^NSData$/,\n /^NSDictionary$/,\n /^NSNumber$/,\n /^NSValue$/,\n /^at_\\d+_[0-9A-F-]+$/i,\n /FileTransferGUIDAttributeName/i,\n /BaseWritingDirectionAttributeName/i,\n /^\\d{2}.*��/,\n];\n\n// biome-ignore lint/suspicious/noControlCharactersInRegex: attributedBody payloads contain binary control bytes.\nconst CONTROL_OR_DEL_RE = /[\\x00-\\x1F\\x7F]/g;\n// biome-ignore lint/suspicious/noControlCharactersInRegex: attributedBody payloads contain binary control bytes.\nconst CONTROL_SPLIT_RE = /[\\x00-\\x1F\\x7F]+/g;\n\n/** Max time (ms) to spend parsing a single attributedBody blob before giving up. */\nconst PARSE_TIMEOUT_MS = 200;\n\nfunction normalizeCandidate(text: string): string | null {\n const normalized = text\n .replace(/^[+;:\"'()&\\s]+/, \"\")\n .replace(/^[^A-Za-z\\d\\s](?=[A-Z])/, \"\")\n .replace(/^[a-z\\d](?=[A-Z])/, \"\")\n // Strip leading single uppercase letter when followed by another uppercase + lowercase\n // (length-byte artifact from typedstream — e.g. \"OYes\" -> \"Yes\" for a 79-char string,\n // \"JThat\" -> \"That\" for a 74-char string). Only triggers when the leading letter is\n // immediately followed by an uppercase + lowercase pattern indicating the real\n // sentence start.\n .replace(/^[A-Z](?=[A-Z][a-z])/, \"\")\n .replace(CONTROL_OR_DEL_RE, \" \")\n .replace(/\\uFFFD+/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (!normalized || normalized === \"�\" || normalized === \"\") return null;\n if (normalized.length < 2 && !/[\\u{1F300}-\\u{1FAFF}]/u.test(normalized)) return null;\n if (/^[A-Za-z0-9]{1,2}$/.test(normalized)) return null;\n if (METADATA_PATTERNS.some((pattern) => pattern.test(normalized))) return null;\n return normalized;\n}\n\nfunction scoreCandidate(text: string): number {\n let score = 0;\n if (/[A-Za-z]/.test(text)) score += 50;\n if (/\\d/.test(text)) score += 10;\n if (/\\s/.test(text)) score += 30;\n if (/[.!?]/.test(text)) score += 15;\n if (/[\\u{1F300}-\\u{1FAFF}]/u.test(text)) score += 100;\n if (/^[A-Za-z0-9]{1,3}$/.test(text)) score -= 100;\n score += Math.min(text.length, 200);\n return score;\n}\n\n/** Boost factor for structured NSString candidates over heuristic byte-scan ones. */\nconst STRUCTURED_BOOST = 500;\n\nexport function extractAttributedBodyText(blob: Buffer | null): string | undefined {\n if (!blob) return undefined;\n\n // Fast path: native Rust parser, when available. We only trust the native\n // result when it looks like a real message (passes our metadata filter and\n // doesn't start with attachment-id markers). Otherwise fall through to the\n // TS implementation, which has more sophisticated heuristics.\n const native = getNativeParser();\n if (native) {\n try {\n const result = native(blob);\n if (result && result.length > 1 && nativeResultLooksTrustworthy(result)) {\n return result;\n }\n } catch {\n // Fall through to TS implementation on any native failure\n }\n }\n\n const parser = new TypedStreamParser(blob);\n const candidates = new Map<string, number>();\n const deadline = Date.now() + PARSE_TIMEOUT_MS;\n\n const remember = (values: string[], boost = 0) => {\n for (const value of values) {\n const normalized = normalizeCandidate(value);\n if (!normalized) continue;\n const score = scoreCandidate(normalized) + boost;\n // Use the highest score we've seen for this candidate\n const prev = candidates.get(normalized) ?? -Infinity;\n if (score > prev) candidates.set(normalized, score);\n }\n };\n\n // Phase 1: structured NSString parsing — these have correct length bytes,\n // so they avoid the \"leading length-byte\" artifact (e.g. \"OYes lmk...\" for a 79-char string).\n // Boost their score so they win over byte-scan fallbacks.\n try {\n if (Date.now() < deadline) {\n remember(\n parser.parseAllNSStrings().map((entry) => entry.content),\n STRUCTURED_BOOST,\n );\n }\n } catch {\n // Ignore parser failures — continue with fallback strategies\n }\n\n // Phase 2: heuristic text extraction (byte-by-byte scan)\n // Lower priority — may pick up length bytes as text artifacts.\n try {\n if (Date.now() < deadline) {\n remember(parser.extractReadableText());\n }\n } catch {\n // Ignore parser failures — continue with fallback strategies\n }\n\n // Phase 3: raw UTF-8 split (always works, cheapest fallback)\n if (Date.now() < deadline) {\n remember(blob.toString(\"utf8\").split(CONTROL_SPLIT_RE));\n }\n\n const best = [...candidates.entries()].sort((a, b) => b[1] - a[1])[0];\n return best?.[0];\n}\n","/**\n * macOS Contacts Database Reader\n *\n * Loads contacts from all available sources:\n * - Local: ~/Library/Application Support/AddressBook/AddressBook-v22.abcddb\n * - iCloud / other accounts: ~/Library/Application Support/AddressBook/Sources/<UUID>/AddressBook-v22.abcddb\n *\n * Merges all into a single lookup so phone/email resolve to display names from any source.\n */\n\nimport { existsSync, readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport Database from \"better-sqlite3\";\n\nexport interface Contact {\n id: number;\n firstName: string | null;\n lastName: string | null;\n middleName: string | null;\n nickname: string | null;\n organization: string | null;\n displayName: string;\n phoneNumbers: string[];\n emails: string[];\n}\n\nexport interface ContactLookup {\n contactId: number;\n displayName: string;\n label?: string;\n}\n\n/**\n * Normalize phone number for comparison (digits only, canonical form).\n * - US: strip leading 1 so 10 digits.\n * - AU/international: keep full digits (e.g. 61... for Australia).\n */\nfunction normalizePhoneNumber(phone: string): string {\n const digits = phone.replace(/\\D/g, \"\");\n // US: 11 digits starting with 1 -> drop 1\n if (digits.length === 11 && digits.startsWith(\"1\")) {\n return digits.slice(1);\n }\n return digits;\n}\n\n/**\n * All normalized forms to use when storing a number in the map,\n * so lookups by +61..., 04..., 0... all hit.\n */\nfunction normalizedPhoneVariants(phone: string): string[] {\n const normalized = normalizePhoneNumber(phone);\n const variants = new Set<string>([normalized]);\n\n // Australia: 61 + 9 digits -> also store bare mobile digits and local 04... format\n if (normalized.length === 11 && normalized.startsWith(\"61\")) {\n const localMobile = normalized.slice(2);\n variants.add(localMobile);\n if (localMobile.length === 9 && localMobile.startsWith(\"4\")) {\n variants.add(`0${localMobile}`);\n }\n }\n\n // Australia: local 04... mobile -> also store bare mobile digits and 61-prefixed format\n if (normalized.length === 10 && normalized.startsWith(\"04\")) {\n const mobileDigits = normalized.slice(1);\n variants.add(mobileDigits);\n variants.add(`61${mobileDigits}`);\n }\n\n // US: also store with leading 1\n if (normalized.length === 10) {\n variants.add(`1${normalized}`);\n }\n // Australia: 9 digits starting with 4 (mobile) -> also store with 61 prefix\n if (normalized.length === 9 && normalized.startsWith(\"4\")) {\n variants.add(`61${normalized}`);\n variants.add(`0${normalized}`);\n }\n\n return [...variants];\n}\n\n/**\n * Normalize email for comparison (lowercase, trim)\n */\nfunction normalizeEmail(email: string): string {\n return email.toLowerCase().trim();\n}\n\nconst ADDRESS_BOOK_DIR = join(homedir(), \"Library\", \"Application Support\", \"AddressBook\");\nconst MAIN_DB_NAME = \"AddressBook-v22.abcddb\";\n\n/**\n * Discover all Address Book database paths: main DB plus each source (e.g. iCloud).\n */\nfunction discoverContactDbPaths(customPaths?: string | string[]): string[] {\n if (customPaths) {\n const list = Array.isArray(customPaths) ? customPaths : [customPaths];\n return list.filter((p) => existsSync(p));\n }\n\n const paths: string[] = [];\n const mainDb = join(ADDRESS_BOOK_DIR, MAIN_DB_NAME);\n if (existsSync(mainDb)) paths.push(mainDb);\n\n const sourcesDir = join(ADDRESS_BOOK_DIR, \"Sources\");\n if (existsSync(sourcesDir)) {\n try {\n const subdirs = readdirSync(sourcesDir, { withFileTypes: true });\n for (const d of subdirs) {\n if (!d.isDirectory()) continue;\n const sourceDb = join(sourcesDir, d.name, MAIN_DB_NAME);\n if (existsSync(sourceDb)) paths.push(sourceDb);\n }\n } catch {\n // ignore read errors\n }\n }\n\n return paths;\n}\n\nexport class ContactsDB {\n private dbPaths: string[] = [];\n private databases: Database.Database[] = [];\n private phoneMap: Map<string, ContactLookup> = new Map();\n private emailMap: Map<string, ContactLookup> = new Map();\n private contactCache: Map<number, Contact> = new Map();\n private initialized = false;\n /** Next id when loading from multiple DBs (avoids Z_PK collisions across sources). */\n private nextContactId = 1;\n\n constructor(dbPaths?: string | string[]) {\n this.dbPaths = discoverContactDbPaths(dbPaths);\n }\n\n /**\n * Initialize the contact lookup maps from all discovered DBs (local + iCloud/sources).\n */\n initialize(): void {\n if (this.initialized) return;\n\n for (const path of this.dbPaths) {\n try {\n const db = new Database(path, { readonly: true });\n this.databases.push(db);\n this.loadContactsFromDb(db);\n } catch (err) {\n console.warn(`ContactsDB: could not load ${path}:`, err);\n }\n }\n\n this.initialized = true;\n }\n\n /**\n * Load contacts from a single Address Book SQLite DB into the shared maps.\n */\n private loadContactsFromDb(db: Database.Database): void {\n const contacts = db\n .prepare(`\n SELECT \n Z_PK as localId,\n ZFIRSTNAME as firstName,\n ZLASTNAME as lastName,\n ZMIDDLENAME as middleName,\n ZNICKNAME as nickname,\n ZORGANIZATION as organization\n FROM ZABCDRECORD\n WHERE ZFIRSTNAME IS NOT NULL OR ZLASTNAME IS NOT NULL OR ZORGANIZATION IS NOT NULL\n `)\n .all() as any[];\n\n for (const row of contacts) {\n const globalId = this.nextContactId++;\n const contact: Contact = {\n id: globalId,\n firstName: row.firstName,\n lastName: row.lastName,\n middleName: row.middleName,\n nickname: row.nickname,\n organization: row.organization,\n displayName: this.buildDisplayName(row),\n phoneNumbers: [],\n emails: [],\n };\n\n const localId = row.localId as number;\n\n const phones = db\n .prepare(`\n SELECT ZFULLNUMBER as number, ZLABEL as label\n FROM ZABCDPHONENUMBER\n WHERE ZOWNER = ? OR Z22_OWNER = ?\n `)\n .all(localId, localId) as any[];\n\n for (const phone of phones) {\n if (phone.number) {\n contact.phoneNumbers.push(phone.number);\n const lookup: ContactLookup = {\n contactId: contact.id,\n displayName: contact.displayName,\n label: phone.label,\n };\n for (const variant of normalizedPhoneVariants(phone.number)) {\n this.phoneMap.set(variant, lookup);\n }\n }\n }\n\n const emails = db\n .prepare(`\n SELECT ZADDRESS as email, ZLABEL as label\n FROM ZABCDEMAILADDRESS\n WHERE ZOWNER = ? OR Z22_OWNER = ?\n `)\n .all(localId, localId) as any[];\n\n for (const email of emails) {\n if (email.email) {\n contact.emails.push(email.email);\n const normalized = normalizeEmail(email.email);\n this.emailMap.set(normalized, {\n contactId: contact.id,\n displayName: contact.displayName,\n label: email.label,\n });\n }\n }\n\n this.contactCache.set(contact.id, contact);\n }\n }\n\n /**\n * Look up a contact by phone number or email.\n */\n lookupContact(handle: string): ContactLookup | null {\n if (!this.initialized) {\n this.initialize();\n }\n\n if (/[\\d+\\-()\\s]/.test(handle)) {\n for (const variant of normalizedPhoneVariants(handle)) {\n const contact = this.phoneMap.get(variant);\n if (contact) {\n return contact;\n }\n }\n }\n\n if (handle.includes(\"@\")) {\n const normalized = normalizeEmail(handle);\n const contact = this.emailMap.get(normalized);\n if (contact) {\n return contact;\n }\n }\n\n return null;\n }\n\n /**\n * Look up a contact by phone number or email\n * Returns display name if found, or the original handle if not\n */\n lookupHandle(handle: string): string {\n return this.lookupContact(handle)?.displayName ?? handle;\n }\n\n /**\n * Get full contact details by ID\n */\n getContact(id: number): Contact | null {\n if (!this.initialized) {\n this.initialize();\n }\n return this.contactCache.get(id) || null;\n }\n\n /**\n * Search contacts by name\n */\n searchContacts(query: string): Contact[] {\n if (!this.initialized) {\n this.initialize();\n }\n\n const lowerQuery = query.toLowerCase();\n return Array.from(this.contactCache.values()).filter(\n (c) =>\n c.displayName.toLowerCase().includes(lowerQuery) ||\n c.phoneNumbers.some((p) => p.includes(query)) ||\n c.emails.some((e) => e.toLowerCase().includes(lowerQuery)),\n );\n }\n\n /**\n * Build display name from contact fields\n */\n private buildDisplayName(row: any): string {\n if (row.nickname) return row.nickname;\n\n const parts: string[] = [];\n\n if (row.firstName) parts.push(row.firstName);\n if (row.lastName) parts.push(row.lastName);\n\n if (parts.length > 0) {\n return parts.join(\" \");\n }\n\n if (row.organization) return row.organization;\n\n return \"Unknown Contact\";\n }\n\n /**\n * Close all database connections\n */\n close(): void {\n for (const db of this.databases) {\n try {\n db.close();\n } catch {\n // ignore\n }\n }\n this.databases = [];\n }\n\n /**\n * Paginated list of all loaded contacts, sorted by displayName.\n * Used by the MCP `list_contacts` tool.\n */\n listContacts(offset = 0, limit = 20): { contacts: Contact[]; total: number } {\n if (!this.initialized) {\n this.initialize();\n }\n const all = Array.from(this.contactCache.values()).sort((a, b) =>\n a.displayName.localeCompare(b.displayName),\n );\n const slice = limit === 0 ? all.slice(offset) : all.slice(offset, offset + limit);\n return { contacts: slice, total: all.length };\n }\n\n /**\n * Get statistics about loaded contacts\n */\n getStats() {\n if (!this.initialized) {\n this.initialize();\n }\n\n return {\n totalContacts: this.contactCache.size,\n phoneNumbers: this.phoneMap.size,\n emails: this.emailMap.size,\n };\n }\n}\n","import { OBJECT_REPLACEMENT_CHAR } from \"./db-schema.js\";\n\nexport function normalizeSnippetText(text: string | null | undefined): string | null {\n if (!text) return null;\n\n const attachmentMarker = new RegExp(\n `${OBJECT_REPLACEMENT_CHAR.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}+`,\n \"g\",\n );\n\n const normalized = text\n .replace(/^[#$]/, \"\")\n .replace(attachmentMarker, \"📎 \")\n .replace(/\\uFFFD/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (!normalized || normalized === \"📎\" || /^(📎\\s*)+$/.test(normalized)) {\n return normalized ? \"(image/attachment)\" : null;\n }\n\n return normalized;\n}\n\nexport function isMetadataOnlySnippet(text: string | null | undefined): boolean {\n if (!text) return false;\n const normalized = text.trim();\n return (\n /^[$#]?https?:\\/\\//i.test(normalized) ||\n /HttpURL\\/?$/i.test(normalized) ||\n /^[$#]https?:\\/\\/.*HttpURL\\/?$/i.test(normalized)\n );\n}\n\nexport function normalizeRichMetadataText(text: string | null | undefined): string | null {\n const normalized = normalizeSnippetText(text);\n if (!normalized) return null;\n\n if (/^https?:\\/\\/.*HttpURL\\/?$/i.test(normalized)) {\n return normalized.replace(/HttpURL\\/?$/i, \"\");\n }\n\n return normalized;\n}\n\nexport function pickConversationSnippet(options: {\n rawText?: string | null;\n parsedText?: string | null;\n summaryText?: string | null;\n}): string | null {\n return (\n normalizeSnippetText(options.rawText) ??\n normalizeSnippetText(options.parsedText) ??\n normalizeSnippetText(options.summaryText)\n );\n}\n","import { parseBuffer } from \"bplist-parser\";\n\nfunction normalizeText(value: string | undefined): string | undefined {\n const normalized = value?.replace(/\\s+/g, \" \").trim();\n return normalized && normalized.length >= 4 ? normalized : undefined;\n}\n\nexport function extractNullPaddedAsciiText(blob: Buffer | null): string | undefined {\n if (!blob) return undefined;\n\n let best = \"\";\n let current = \"\";\n\n const flush = () => {\n if (current.length > best.length) {\n best = current;\n }\n current = \"\";\n };\n\n for (let index = 0; index < blob.length - 1; index += 1) {\n const first = blob[index];\n const second = blob[index + 1];\n\n if (first === 0 && second >= 32 && second <= 126) {\n current += String.fromCharCode(second);\n index += 1;\n continue;\n }\n\n flush();\n }\n\n flush();\n\n return normalizeText(best);\n}\n\nexport function extractArchivedAttributedStringText(blob: Buffer | null): string | undefined {\n if (!blob) return undefined;\n\n try {\n const [plist] = parseBuffer(blob) as Array<{ $objects?: unknown[] }>;\n const objects = plist?.$objects;\n if (!Array.isArray(objects)) {\n return extractNullPaddedAsciiText(blob);\n }\n\n const attributedString = objects.find(\n (value): value is { NSString?: { UID?: number } } =>\n typeof value === \"object\" && value !== null && \"NSString\" in value,\n );\n const stringUid = attributedString?.NSString?.UID;\n if (typeof stringUid === \"number\" && typeof objects[stringUid] === \"string\") {\n return normalizeText(objects[stringUid] as string);\n }\n\n const firstString = objects.find(\n (value): value is string =>\n typeof value === \"string\" && !value.startsWith(\"$\") && !value.startsWith(\"NS\"),\n );\n return normalizeText(firstString) ?? extractNullPaddedAsciiText(blob);\n } catch {\n return extractNullPaddedAsciiText(blob);\n }\n}\n\nexport function extractChatSummaryText(blob: Buffer | null): string | undefined {\n if (!blob) return undefined;\n\n try {\n const [plist] = parseBuffer(blob) as Array<{\n chatSummaryDictionary?: { chatSummary?: Buffer | null };\n }>;\n const chatSummary = plist?.chatSummaryDictionary?.chatSummary;\n if (chatSummary) {\n return extractArchivedAttributedStringText(chatSummary);\n }\n } catch {\n // Fall back to heuristic extraction below.\n }\n\n return extractNullPaddedAsciiText(blob);\n}\n","/**\n * Persistent SQLite store for conversation thread slugs.\n * Stores slug-to-chat mappings at ~/.imsg-mcp/slugs.db so they survive\n * MCP server restarts and can be referenced by agents/scripts.\n */\n\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport Database from \"better-sqlite3\";\n\nexport interface SlugRecord {\n slug: string;\n chatGuid: string;\n chatIdentifier: string;\n displayName: string | null;\n service: string;\n isGroup: boolean;\n participants: string;\n updatedAt: number;\n}\n\nconst DEFAULT_DIR = join(homedir(), \".imsg-mcp\");\nconst DEFAULT_DB = join(DEFAULT_DIR, \"slugs.db\");\n\nexport class SlugStore {\n private db: Database.Database;\n\n constructor(dbPath?: string) {\n const path = dbPath ?? DEFAULT_DB;\n const dir = dirname(path);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n this.db = new Database(path);\n this.db.pragma(\"journal_mode = WAL\");\n this.migrate();\n }\n\n private migrate(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS thread_slugs (\n slug TEXT PRIMARY KEY,\n chat_guid TEXT UNIQUE NOT NULL,\n chat_identifier TEXT NOT NULL,\n display_name TEXT,\n service TEXT NOT NULL DEFAULT 'iMessage',\n is_group INTEGER NOT NULL DEFAULT 0,\n participants TEXT NOT NULL DEFAULT '',\n updated_at INTEGER NOT NULL DEFAULT 0\n )\n `);\n }\n\n upsert(record: SlugRecord): void {\n const params = {\n slug: record.slug,\n chatGuid: record.chatGuid,\n chatIdentifier: record.chatIdentifier,\n displayName: record.displayName,\n service: record.service,\n isGroup: record.isGroup ? 1 : 0,\n participants: record.participants,\n updatedAt: record.updatedAt,\n };\n\n const updateByGuid = this.db.prepare(`\n UPDATE thread_slugs\n SET\n slug = @slug,\n chat_identifier = @chatIdentifier,\n display_name = @displayName,\n service = @service,\n is_group = @isGroup,\n participants = @participants,\n updated_at = @updatedAt\n WHERE chat_guid = @chatGuid\n `);\n\n const updateResult = updateByGuid.run(params);\n if (updateResult.changes > 0) return;\n\n const insertOrUpdateBySlug = this.db.prepare(`\n INSERT INTO thread_slugs (slug, chat_guid, chat_identifier, display_name, service, is_group, participants, updated_at)\n VALUES (@slug, @chatGuid, @chatIdentifier, @displayName, @service, @isGroup, @participants, @updatedAt)\n ON CONFLICT(slug) DO UPDATE SET\n chat_guid = excluded.chat_guid,\n chat_identifier = excluded.chat_identifier,\n display_name = excluded.display_name,\n service = excluded.service,\n is_group = excluded.is_group,\n participants = excluded.participants,\n updated_at = excluded.updated_at\n `);\n\n insertOrUpdateBySlug.run(params);\n }\n\n upsertMany(records: SlugRecord[]): void {\n const tx = this.db.transaction((recs: SlugRecord[]) => {\n for (const r of recs) this.upsert(r);\n });\n tx(records);\n }\n\n lookupBySlug(slug: string): SlugRecord | null {\n const row = this.db.prepare(\"SELECT * FROM thread_slugs WHERE slug = ?\").get(slug) as any;\n return row ? this.rowToRecord(row) : null;\n }\n\n lookupByGuid(chatGuid: string): SlugRecord | null {\n const row = this.db\n .prepare(\"SELECT * FROM thread_slugs WHERE chat_guid = ?\")\n .get(chatGuid) as any;\n return row ? this.rowToRecord(row) : null;\n }\n\n lookupByChatIdentifier(chatIdentifier: string): SlugRecord | null {\n const row = this.db\n .prepare(\"SELECT * FROM thread_slugs WHERE chat_identifier = ?\")\n .get(chatIdentifier) as any;\n return row ? this.rowToRecord(row) : null;\n }\n\n all(): SlugRecord[] {\n const rows = this.db\n .prepare(\"SELECT * FROM thread_slugs ORDER BY updated_at DESC\")\n .all() as any[];\n return rows.map((r) => this.rowToRecord(r));\n }\n\n /** Remove slugs whose chat_guid is not in the given set of valid guids. */\n prune(validGuids: Set<string>): number {\n const all = this.db.prepare(\"SELECT slug, chat_guid FROM thread_slugs\").all() as {\n slug: string;\n chat_guid: string;\n }[];\n const toDelete = all.filter((r) => !validGuids.has(r.chat_guid));\n if (toDelete.length === 0) return 0;\n const del = this.db.prepare(\"DELETE FROM thread_slugs WHERE slug = ?\");\n const tx = this.db.transaction((slugs: string[]) => {\n for (const s of slugs) del.run(s);\n });\n tx(toDelete.map((r) => r.slug));\n return toDelete.length;\n }\n\n close(): void {\n this.db.close();\n }\n\n private rowToRecord(row: any): SlugRecord {\n return {\n slug: row.slug,\n chatGuid: row.chat_guid,\n chatIdentifier: row.chat_identifier,\n displayName: row.display_name,\n service: row.service,\n isGroup: Boolean(row.is_group),\n participants: row.participants,\n updatedAt: row.updated_at,\n };\n }\n}\n","/**\n * iMessage Database Reader\n *\n * All database access goes through a single better-sqlite3 connection.\n * attributedBody parsing uses our local TypedStreamParser fork.\n *\n * See docs/IMESSAGE_DB_SCHEMA.md for database structure reference.\n * Schema constants and epoch/timestamp helpers live in db-schema.ts.\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport Database from \"better-sqlite3\";\n\n/** Minimal chat row from the chat table. */\nexport interface ChatRow {\n ROWID: number;\n guid: string;\n chat_identifier: string;\n display_name: string | null;\n}\n\n/** Raw message row from the message table (columns used throughout this module). */\nexport interface MessageRow {\n ROWID: number;\n guid: string;\n text: string | null;\n attributedBody: Buffer | null;\n date: number;\n is_from_me: number;\n handle_id: string | null;\n cache_has_attachments: number;\n}\n\nimport { extractAttributedBodyText } from \"./attributed-body-text.js\";\nimport { ContactsDB } from \"./contacts-db.js\";\nimport {\n isMetadataOnlySnippet,\n normalizeRichMetadataText,\n pickConversationSnippet,\n} from \"./conversation-snippet.js\";\nimport {\n AssociatedMessageType,\n isReactionType,\n MAC_EPOCH_OFFSET,\n macAutoTimestampToDate,\n NANOS_PER_SECOND,\n OBJECT_REPLACEMENT_CHAR,\n macTimestampToDate as schemaMacTimestampToDate,\n parseAssociatedMessageGuid as schemaParseAssociatedMessageGuid,\n Tables,\n} from \"./db-schema.js\";\nimport { perf } from \"./logger.js\";\nimport { extractChatSummaryText } from \"./plist-text.js\";\nimport { type SlugRecord, SlugStore } from \"./slug-store.js\";\nimport { generateThreadSlug, isGroupChatIdentifier, isGroupGuid } from \"./thread-slug.js\";\nimport type {\n Attachment,\n Conversation,\n Message,\n Reaction,\n ReplyContext,\n RichContentType,\n TapbackType,\n} from \"./types.js\";\n\n/** Chat row with last message date and service from join (for sorting by activity). */\ntype ChatWithLastDate = ChatRow & { last_date: number | null; service_name?: string | null };\ntype LastConversationRow = {\n chatId: number;\n lastDate: number;\n lastMessageId: number;\n lastService: string | null;\n lastIsFromMe: boolean;\n balloonBundleId: string | null;\n snippet: string | null;\n chatProperties: Buffer | null;\n};\ntype PreparedConversationEntry = {\n last?: LastConversationRow;\n mergeKey: string;\n conversation: Conversation;\n};\n\nfunction toChatRow(c: ChatWithLastDate): ChatRow {\n return {\n ROWID: c.ROWID,\n guid: c.guid,\n chat_identifier: c.chat_identifier,\n display_name: c.display_name,\n };\n}\n\n/**\n * Tapback type codes from iMessage database\n * 2000-2005: Add reaction, 3000-3005: Remove reaction\n */\nconst TAPBACK_TYPE_MAP: Record<number, { type: TapbackType; isRemoval: boolean }> = {\n 2000: { type: \"love\", isRemoval: false },\n 2001: { type: \"like\", isRemoval: false },\n 2002: { type: \"dislike\", isRemoval: false },\n 2003: { type: \"laugh\", isRemoval: false },\n 2004: { type: \"emphasize\", isRemoval: false },\n 2005: { type: \"question\", isRemoval: false },\n 2006: { type: \"emoji\", isRemoval: false }, // iOS 18+ custom emoji\n 3000: { type: \"love\", isRemoval: true },\n 3001: { type: \"like\", isRemoval: true },\n 3002: { type: \"dislike\", isRemoval: true },\n 3003: { type: \"laugh\", isRemoval: true },\n 3004: { type: \"emphasize\", isRemoval: true },\n 3005: { type: \"question\", isRemoval: true },\n 3006: { type: \"emoji\", isRemoval: true },\n 1000: { type: \"sticker\", isRemoval: false },\n};\n\n/** Use schema helper for parsing associated_message_guid. */\nconst parseAssociatedMessageGuid = schemaParseAssociatedMessageGuid;\n\nfunction isPlaceholderText(text: string | null | undefined): boolean {\n if (!text) return true;\n return /^[\\uFFFC\\uFFFD\\s]+$/u.test(text);\n}\n\nfunction isHiddenSystemItem(itemType: number | null | undefined): boolean {\n return (itemType ?? 0) !== 0;\n}\n\n/**\n * Determine rich content type from balloon_bundle_id\n */\nfunction getRichContentType(balloonBundleId: string | null): RichContentType | undefined {\n if (!balloonBundleId) return undefined;\n\n if (balloonBundleId === \"com.apple.messages.URLBalloonProvider\") {\n return \"link_preview\";\n }\n if (balloonBundleId === \"com.apple.DigitalTouchBalloonProvider\") {\n return \"digital_touch\";\n }\n if (balloonBundleId === \"com.apple.Handwriting.HandwritingProvider\") {\n return \"handwriting\";\n }\n if (balloonBundleId.includes(\"findmy\") || balloonBundleId.includes(\"Maps\")) {\n return \"location\";\n }\n if (balloonBundleId.includes(\"MSMessageExtensionBalloonPlugin\")) {\n return \"app_message\";\n }\n return \"unknown\";\n}\n\n/** Use schema helper for Mac epoch timestamps. */\nconst macTimestampToDate = schemaMacTimestampToDate;\n\nexport class IMessageDB {\n private raw: Database.Database;\n private dbPath: string;\n /** Address-book reader. Public so MCP `*_contacts` tools can wrap it. */\n readonly contacts: ContactsDB;\n private slugStore: SlugStore;\n /** In-memory slug -> ChatWithLastDate for fast lookups during a session. */\n private slugMap = new Map<string, ChatWithLastDate>();\n /** In-memory chat guid -> slug for stable per-chat slug lookups. */\n private guidToSlug = new Map<string, string>();\n\n // ── TTL caches ───────────────────────────────────────────────────────\n private static readonly CACHE_TTL_MS = 30_000;\n private cachedAllChats: { data: ChatWithLastDate[]; ts: number } | null = null;\n private cachedLastByChat: { data: Record<number, LastConversationRow>; ts: number } | null = null;\n private cachedUnreadByChat: { data: Record<number, number>; ts: number } | null = null;\n private cachedParticipants = new Map<number, string[]>();\n private cachedMergeKeys = new Map<string, string>();\n private cachedSnippets = new Map<number, string | null>();\n private backgroundSyncNeeded = true;\n private backgroundRefreshScheduled = false;\n\n constructor(dbPath?: string, contactsDbPaths?: string | string[], slugStorePath?: string) {\n const span = perf(\"IMessageDB.constructor\");\n this.dbPath = dbPath || join(homedir(), \"Library\", \"Messages\", \"chat.db\");\n this.raw = new Database(this.dbPath, { readonly: true });\n this.contacts = new ContactsDB(contactsDbPaths);\n this.slugStore = new SlugStore(slugStorePath);\n\n const contactsSpan = perf(\"contacts.initialize\");\n try {\n this.contacts.initialize();\n contactsSpan.end();\n } catch (err) {\n contactsSpan.end({ error: String(err) });\n console.warn(\"Failed to initialize contacts database:\", err);\n }\n\n this.loadCachedSlugs();\n span.end();\n }\n\n /**\n * Fast startup: load persisted slugs from SlugStore into in-memory maps.\n * No chat.db queries -- uses the SQLite slug store populated by previous runs.\n */\n private loadCachedSlugs(): void {\n const span = perf(\"loadCachedSlugs\");\n const records = this.slugStore.all();\n for (const r of records) {\n this.guidToSlug.set(r.chatGuid, r.slug);\n this.slugMap.set(r.slug, {\n ROWID: 0, // placeholder -- will be filled on full sync\n guid: r.chatGuid,\n chat_identifier: r.chatIdentifier,\n display_name: r.displayName,\n service_name: r.service ?? null,\n last_date: null,\n });\n }\n span.end({ loaded: records.length });\n }\n\n /** Generate and cache a slug for a single chat. */\n private syncSlugForChat(chat: ChatWithLastDate): string {\n const isGroup = isGroupGuid(chat.guid) || isGroupChatIdentifier(chat.chat_identifier);\n const resolvedName =\n !isGroup && chat.chat_identifier ? this.contacts.lookupHandle(chat.chat_identifier) : null;\n\n const slug = generateThreadSlug({\n chatIdentifier: chat.chat_identifier,\n guid: chat.guid,\n displayName: chat.display_name,\n serviceName: chat.service_name ?? null,\n resolvedContactName: resolvedName !== chat.chat_identifier ? resolvedName : null,\n });\n\n const participants = isGroup ? this.fetchChatParticipants(chat.ROWID) : [chat.chat_identifier];\n\n this.slugStore.upsert({\n slug,\n chatGuid: chat.guid,\n chatIdentifier: chat.chat_identifier,\n displayName: resolvedName !== chat.chat_identifier ? resolvedName : chat.display_name || null,\n service: this.detectServiceForChat(chat),\n isGroup,\n participants: participants.join(\",\"),\n updatedAt: Date.now(),\n });\n\n this.slugMap.set(slug, chat);\n this.guidToSlug.set(chat.guid, slug);\n return slug;\n }\n\n /**\n * Schedule a full slug sync in the background using setImmediate chunks.\n * Each chunk processes up to 100 chats then yields the event loop.\n */\n scheduleBackgroundSlugSync(): void {\n if (!this.backgroundSyncNeeded) return;\n this.backgroundSyncNeeded = false;\n\n const span = perf(\"backgroundSlugSync\");\n const chats = this.getAllChatsWithLastDate();\n const validGuids = new Set<string>();\n let index = 0;\n let synced = 0;\n const CHUNK = 100;\n\n const processChunk = () => {\n const end = Math.min(index + CHUNK, chats.length);\n for (; index < end; index++) {\n const chat = chats[index];\n validGuids.add(chat.guid);\n if (!this.guidToSlug.has(chat.guid)) {\n this.syncSlugForChat(chat);\n synced++;\n } else {\n // Update the in-memory map with fresh data (ROWID, last_date)\n const existingSlug = this.guidToSlug.get(chat.guid)!;\n this.slugMap.set(existingSlug, chat);\n }\n }\n if (index < chats.length) {\n setImmediate(processChunk);\n } else {\n this.slugStore.prune(validGuids);\n span.end({ chats: chats.length, newSlugs: synced });\n }\n };\n\n setImmediate(processChunk);\n }\n\n /** Look up a chat by thread slug. */\n findChatBySlug(slug: string): ChatRow | null {\n const cached = this.slugMap.get(slug);\n if (cached) return toChatRow(cached);\n const record = this.slugStore.lookupBySlug(slug);\n if (!record) return null;\n return this.findChatByIdentifier(record.chatIdentifier);\n }\n\n /** Get the slug for a specific chat GUID. */\n getSlugForChatGuid(chatGuid: string): string | null {\n const cached = this.guidToSlug.get(chatGuid);\n if (cached) return cached;\n const record = this.slugStore.lookupByGuid(chatGuid);\n return record?.slug ?? null;\n }\n\n /** Get the slug record for a chat_identifier (for attaching to output). */\n getSlugForChatIdentifier(chatIdentifier: string): string | null {\n const matches = [...this.slugMap.entries()].filter(\n ([, chat]) => chat.chat_identifier === chatIdentifier,\n );\n if (matches.length === 1) return matches[0][0];\n if (matches.length > 1) return null;\n const record = this.slugStore.lookupByChatIdentifier(chatIdentifier);\n return record?.slug ?? null;\n }\n\n /** Get slug record by slug string. */\n getSlugRecord(slug: string): SlugRecord | null {\n return this.slugStore.lookupBySlug(slug);\n }\n\n /** Get all slug records. */\n getAllSlugs(): SlugRecord[] {\n return this.slugStore.all();\n }\n\n /** Fetch group chat participants from chat_handle_join. */\n private fetchChatParticipants(chatRowId: number): string[] {\n const cached = this.cachedParticipants.get(chatRowId);\n if (cached) return cached;\n const stmt = this.raw.prepare(`\n SELECT h.id\n FROM ${Tables.CHAT_HANDLE_JOIN} chj\n JOIN ${Tables.HANDLE} h ON chj.handle_id = h.ROWID\n WHERE chj.chat_id = ?\n `);\n const rows = stmt.all(chatRowId) as { id: string }[];\n const result = rows.map((r) => r.id);\n this.cachedParticipants.set(chatRowId, result);\n return result;\n }\n\n /** Detect service type from chat data. */\n private detectServiceForChat(chat: ChatWithLastDate): \"iMessage\" | \"SMS\" {\n if (chat.service_name) {\n return chat.service_name.toLowerCase().includes(\"sms\") ? \"SMS\" : \"iMessage\";\n }\n if (chat.guid) {\n return chat.guid.toLowerCase().startsWith(\"sms\") ? \"SMS\" : \"iMessage\";\n }\n return \"iMessage\";\n }\n\n /**\n * Get the N most recent messages across all conversations\n * By default excludes reactions (tapbacks) for cleaner output\n */\n async getRecentMessages(\n limit: number = 20,\n includeReactions: boolean = false,\n ): Promise<Message[]> {\n const span = perf(\"getRecentMessages\");\n const chats = this.getAllChatsWithLastDate()\n .sort((a, b) => (b.last_date ?? 0) - (a.last_date ?? 0))\n .slice(0, 10);\n\n const allMessages: Message[] = [];\n\n for (const chat of chats) {\n try {\n const rows = this.fetchMessagesForChatRowId(chat.ROWID, Math.min(limit * 2, 40));\n const extBatch = this.fetchExtendedMessageDataBatch(rows.map((r) => r.ROWID));\n\n for (const msg of rows) {\n const text = this.parseMessageText(msg);\n const ext = extBatch.get(msg.ROWID) ?? {};\n if (isHiddenSystemItem(ext.item_type)) continue;\n const converted = this.convertMessage(msg, text, chat.chat_identifier, ext);\n\n if (!includeReactions && converted.isReaction) continue;\n\n allMessages.push(converted);\n }\n } catch {\n // Skip chats that fail to load\n }\n }\n\n const result = allMessages.sort((a, b) => b.date.getTime() - a.date.getTime()).slice(0, limit);\n span.end({ limit, returned: result.length });\n return result;\n }\n\n /**\n * Get messages from a specific conversation, sorted by date ascending (chronological).\n * By default excludes reactions (tapbacks) for cleaner output.\n */\n async getMessagesForChat(\n chatIdentifier: string,\n limit: number = 50,\n options: {\n includeReactions?: boolean;\n includeReactionDetails?: boolean;\n beforeMessageId?: number;\n afterMessageId?: number;\n } = {},\n ): Promise<Message[]> {\n const span = perf(\"getMessagesForChat\");\n const {\n includeReactions = false,\n includeReactionDetails = false,\n beforeMessageId,\n afterMessageId,\n } = options;\n const chats = this.resolveChatsForConversation(chatIdentifier);\n if (chats.length === 0) {\n span.end({ limit, returned: 0 });\n return [];\n }\n\n const perChatLimit = Math.max(limit * 2, 50);\n const result = new Map<number, Message>();\n for (const chat of chats) {\n const rows = this.fetchMessagesForChatRowId(\n chat.ROWID,\n perChatLimit,\n beforeMessageId,\n afterMessageId,\n );\n const extBatch = this.fetchExtendedMessageDataBatch(rows.map((r) => r.ROWID));\n\n // Batch-fetch reactions for the entire chat instead of per-message LIKE queries\n const reactionsByGuid = includeReactionDetails\n ? this.fetchReactionsForChat(chat.ROWID)\n : undefined;\n\n for (const msg of rows) {\n const text = this.parseMessageText(msg);\n const ext = extBatch.get(msg.ROWID) ?? {};\n if (isHiddenSystemItem(ext.item_type)) continue;\n const converted = this.convertMessage(\n msg,\n text,\n chat.chat_identifier,\n ext,\n false, // don't let convertMessage fetch reactions individually\n );\n\n // Attach pre-fetched reactions\n if (reactionsByGuid && !converted.isReaction) {\n const rxns = reactionsByGuid.get(msg.guid);\n if (rxns && rxns.length > 0) {\n converted.reactions = this.consolidateReactions(rxns);\n if (converted.reactions.length === 0) converted.reactions = undefined;\n }\n }\n\n if (!includeReactions && converted.isReaction) continue;\n\n result.set(converted.id, converted);\n }\n }\n\n // Sort by timestamp ascending (chronological conversation order)\n const sorted = [...result.values()]\n .sort((a, b) => a.date.getTime() - b.date.getTime())\n .slice(-limit);\n span.end({ limit, chats: chats.length, returned: sorted.length });\n return sorted;\n }\n\n /**\n * Get unread messages across all conversations, sorted by date descending (newest first).\n * Excludes reactions for cleaner output.\n * @param limit Max number of messages to return (default 100).\n */\n async getUnreadMessages(limit: number = 100): Promise<Message[]> {\n const span = perf(\"getUnreadMessages\");\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID,\n m.guid,\n m.text,\n m.attributedBody,\n m.date,\n m.is_from_me,\n h.id as handle_id,\n m.cache_has_attachments,\n c.chat_identifier\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n LEFT JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID\n WHERE m.is_from_me = 0\n AND m.is_read = 0\n AND m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n ORDER BY m.date DESC\n LIMIT ?\n `);\n\n const rows = stmt.all(limit) as (MessageRow & { chat_identifier: string | null })[];\n const validRows = rows.filter((r) => r.chat_identifier != null);\n const extBatch = this.fetchExtendedMessageDataBatch(validRows.map((r) => r.ROWID));\n\n const result: Message[] = [];\n for (const row of validRows) {\n const text = this.parseMessageText(row);\n const ext = extBatch.get(row.ROWID) ?? {};\n result.push(this.convertMessage(row, text, row.chat_identifier!, ext));\n }\n\n result.sort((a, b) => b.date.getTime() - a.date.getTime());\n const sliced = result.slice(0, limit);\n span.end({ limit, rows: rows.length, returned: sliced.length });\n return sliced;\n }\n\n /**\n * Get the most recent message in a conversation\n */\n async getLastMessage(chatIdentifier: string): Promise<Message | null> {\n const messages = await this.getMessagesForChat(chatIdentifier, 1);\n return messages.length > 0 ? messages[messages.length - 1] : null;\n }\n\n /**\n * Get messages after a specific message ID (for polling new messages).\n * Returns incoming messages only, sorted by date ascending (chronological).\n */\n async getMessagesAfter(chatIdentifier: string, afterMessageId: number): Promise<Message[]> {\n const messages = await this.getMessagesForChat(chatIdentifier, 1000, { afterMessageId });\n const filtered = messages.filter((m) => m.id > afterMessageId && !m.isFromMe);\n filtered.sort((a, b) => a.date.getTime() - b.date.getTime());\n return filtered;\n }\n\n /**\n * List all conversations with metadata, sorted by last message date (newest first).\n * Populates lastMessageDate, lastMessageSnippet, and unreadCount to match Messages.app left pane.\n */\n async listConversations(limit: number = 200): Promise<Conversation[]> {\n const span = perf(\"listConversations\");\n const chats = this.getAllChats();\n const lastByChat = this.getLastMessageByChat();\n const unreadByChat = this.getUnreadByChat();\n\n // ── Pass 1: lightweight sort entries for ALL chats (no DB lookups) ──\n type SortEntry = {\n chat: ChatRow;\n lastDate: number;\n isGroup: boolean;\n last?: LastConversationRow;\n };\n const sortEntries: SortEntry[] = chats.map((chat) => ({\n chat,\n lastDate: lastByChat[chat.ROWID]?.lastDate ?? 0,\n isGroup: isGroupGuid(chat.guid) || isGroupChatIdentifier(chat.chat_identifier),\n last: lastByChat[chat.ROWID],\n }));\n\n // Sort by last message date descending\n sortEntries.sort((a, b) => b.lastDate - a.lastDate);\n\n // Over-fetch to account for dedup (multiple chat rows can merge into one conversation)\n const candidates = sortEntries.slice(0, limit * 3);\n\n // ── Pass 2: full enrichment only on candidates ──\n const prepared = candidates.map(({ chat, isGroup, last }) => {\n const lastDate = last ? macTimestampToDate(last.lastDate) : null;\n const rawIdentifier = chat.chat_identifier;\n\n let displayName = chat.display_name;\n if (!displayName && rawIdentifier && !isGroup) {\n const resolved = this.contacts.lookupHandle(rawIdentifier);\n displayName = resolved !== rawIdentifier ? resolved : null;\n }\n\n const participants = isGroup ? this.fetchChatParticipants(chat.ROWID) : [rawIdentifier];\n const mergeKey = this.getConversationMergeKey(rawIdentifier, chat.guid, isGroup);\n const slug = this.getSlugForChatGuid(chat.guid) ?? rawIdentifier;\n const chatData = this.slugMap.get(slug);\n const serviceType = chatData ? this.detectServiceForChat(chatData) : \"iMessage\";\n\n return {\n last,\n mergeKey,\n conversation: {\n chatId: chat.guid,\n chatIdentifier: rawIdentifier,\n displayName: displayName || null,\n rawIdentifier,\n participants,\n lastMessageDate: lastDate,\n lastMessageSnippet: null,\n unreadCount: unreadByChat[chat.ROWID] ?? 0,\n threadSlug: slug,\n isGroupChat: isGroup,\n serviceType,\n } satisfies Conversation,\n };\n });\n\n const deduped = this.mergeDuplicateConversations(prepared);\n const selected = deduped.slice(0, limit);\n\n const result = selected.map(({ conversation, last }) => ({\n ...conversation,\n lastMessageSnippet: this.resolveConversationSnippet(last),\n }));\n span.end({\n chats: chats.length,\n candidates: candidates.length,\n deduped: deduped.length,\n returned: result.length,\n });\n return result;\n }\n\n /**\n * Find a chat by phone number, email, or chat identifier.\n * Uses a single join (chats + last message date), then filters and sorts by date.\n * When multiple chats match (e.g. same number, different threads), returns the one with the most recent message.\n */\n async findChatByHandle(handle: string): Promise<Conversation | null> {\n const chats = this.getAllChatsWithLastDate();\n\n // Normalize the search handle\n const normalized = handle.replace(/[\\s\\-()]/g, \"\").toLowerCase();\n\n const matches = chats.filter((chat) => {\n const chatNorm = chat.chat_identifier?.replace(/[\\s\\-()]/g, \"\").toLowerCase() || \"\";\n return chatNorm.includes(normalized) || normalized.includes(chatNorm);\n });\n\n if (matches.length === 0) return null;\n\n const found = this.pickMostRecentChat(matches);\n const rawIdentifier = found.chat_identifier;\n const isGroup = isGroupGuid(found.guid) || isGroupChatIdentifier(rawIdentifier);\n let displayName = found.display_name;\n if (!displayName && rawIdentifier && !isGroup) {\n const resolved = this.contacts.lookupHandle(rawIdentifier);\n displayName = resolved !== rawIdentifier ? resolved : null;\n }\n\n const slug = this.getSlugForChatGuid(found.guid) ?? rawIdentifier;\n const chatData = this.slugMap.get(slug);\n const serviceType = chatData ? this.detectServiceForChat(chatData) : \"iMessage\";\n\n return {\n chatId: found.guid,\n chatIdentifier: rawIdentifier,\n displayName: displayName || null,\n rawIdentifier,\n participants: isGroup ? this.fetchChatParticipants(found.ROWID) : [rawIdentifier],\n lastMessageDate: null,\n lastMessageSnippet: null,\n unreadCount: 0,\n threadSlug: slug,\n isGroupChat: isGroup,\n serviceType,\n };\n }\n\n /**\n * Search messages across all conversations.\n *\n * Strategy: search the `text` column via LIKE first (covers most messages).\n * For messages where text is NULL but attributedBody exists, we fetch a larger\n * window and post-filter after parsing the blob. The upstream imessage-parser\n * had a bug here: its SQL `WHERE text LIKE ? OR attributedBody IS NOT NULL`\n * with a LIMIT meant the LIMIT was consumed by non-matching attributedBody rows,\n * hiding older text matches entirely.\n */\n async searchMessages(query: string, limit: number = 20): Promise<Message[]> {\n const span = perf(\"searchMessages\");\n // Phase 1: direct text match -- fast and reliable\n const textStmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments,\n c.chat_identifier\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n LEFT JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID\n WHERE m.text LIKE ?\n AND m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n ORDER BY m.date DESC\n LIMIT ?\n `);\n\n const textRows = textStmt.all(`%${query}%`, limit * 2) as (MessageRow & {\n chat_identifier: string | null;\n })[];\n const seenIds = new Set<number>();\n const messages: Message[] = [];\n\n for (const row of textRows) {\n if (!row.chat_identifier) continue;\n seenIds.add(row.ROWID);\n const text = this.parseMessageText(row);\n const ext = this.fetchExtendedMessageData(row.ROWID);\n if (isHiddenSystemItem(ext.item_type)) continue;\n const converted = this.convertMessage(row, text, row.chat_identifier, ext);\n if (converted.isReaction) continue;\n messages.push(converted);\n if (messages.length >= limit) return messages;\n }\n\n // Phase 2: scan attributedBody-only messages (text IS NULL) for matches\n // Use a generous window so we don't miss older messages\n const blobStmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments,\n c.chat_identifier\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n LEFT JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID\n WHERE m.text IS NULL\n AND m.attributedBody IS NOT NULL\n AND m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n ORDER BY m.date DESC\n LIMIT ?\n `);\n\n const blobRows = blobStmt.all(limit * 20) as (MessageRow & {\n chat_identifier: string | null;\n })[];\n const queryLower = query.toLowerCase();\n\n for (const row of blobRows) {\n if (!row.chat_identifier || seenIds.has(row.ROWID)) continue;\n const text = this.parseMessageText(row);\n if (!text || !text.toLowerCase().includes(queryLower)) continue;\n const ext = this.fetchExtendedMessageData(row.ROWID);\n if (isHiddenSystemItem(ext.item_type)) continue;\n const converted = this.convertMessage(row, text, row.chat_identifier, ext);\n if (converted.isReaction) continue;\n messages.push(converted);\n if (messages.length >= limit) break;\n }\n\n span.end({\n query,\n limit,\n textHits: textRows.length,\n blobScanned: blobRows.length,\n returned: messages.length,\n });\n return messages;\n }\n\n /**\n * Get a deep link to open a specific conversation in Messages.app\n */\n getConversationLink(chatIdentifier: string): string {\n return `imessage://${encodeURIComponent(chatIdentifier)}`;\n }\n\n private resolveConversationSnippet(last?: LastConversationRow): string | null {\n if (!last) return null;\n const cached = this.cachedSnippets.get(last.lastMessageId);\n if (cached !== undefined) return cached;\n\n let result: string | null = null;\n\n const directSnippet = pickConversationSnippet({ rawText: last.snippet });\n if (directSnippet && !this.shouldFallbackToPreviousSnippet(directSnippet, last)) {\n result = directSnippet;\n }\n\n if (result == null) {\n const parsedSnippet = pickConversationSnippet({\n parsedText: this.getMessageTextByRowId(last.lastMessageId),\n });\n if (parsedSnippet && !this.shouldFallbackToPreviousSnippet(parsedSnippet, last)) {\n result = parsedSnippet;\n }\n }\n\n if (result == null) {\n result = this.getPreviousConversationSnippet(last);\n }\n\n if (result == null) {\n result = pickConversationSnippet({\n summaryText: extractChatSummaryText(last.chatProperties) ?? null,\n });\n }\n\n this.cachedSnippets.set(last.lastMessageId, result);\n return result;\n }\n\n private shouldFallbackToPreviousSnippet(snippet: string, last: LastConversationRow): boolean {\n return Boolean(\n last.balloonBundleId?.includes(\"URLBalloonProvider\") && isMetadataOnlySnippet(snippet),\n );\n }\n\n private getPreviousConversationSnippet(last: LastConversationRow): string | null {\n const rows = this.raw\n .prepare(`\n SELECT m.ROWID, m.text, m.attributedBody, m.date, m.is_from_me\n FROM ${Tables.MESSAGE} m\n JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON cmj.message_id = m.ROWID\n WHERE cmj.chat_id = ?\n AND m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n AND (m.date < ? OR (m.date = ? AND m.ROWID < ?))\n ORDER BY m.date DESC, m.ROWID DESC\n LIMIT 5\n `)\n .all(last.chatId, last.lastDate, last.lastDate, last.lastMessageId) as Array<{\n ROWID: number;\n text: string | null;\n attributedBody: Buffer | null;\n date: number;\n is_from_me: number;\n }>;\n\n for (const row of rows) {\n if (Boolean(row.is_from_me) !== last.lastIsFromMe) break;\n if (Math.abs(last.lastDate - row.date) > 60_000_000_000) break;\n\n const snippet = pickConversationSnippet({\n rawText: row.text,\n parsedText: this.extractMessageText(row),\n });\n\n if (snippet && !isMetadataOnlySnippet(snippet)) {\n return snippet;\n }\n }\n\n return null;\n }\n\n private mergeDuplicateConversations(prepared: PreparedConversationEntry[]) {\n const merged: typeof prepared = [];\n const indexByKey = new Map<string, number>();\n\n for (const entry of prepared) {\n const existingIndex = indexByKey.get(entry.mergeKey);\n if (existingIndex === undefined) {\n indexByKey.set(entry.mergeKey, merged.length);\n merged.push(entry);\n continue;\n }\n\n merged[existingIndex] = this.mergeConversationEntries(merged[existingIndex], entry);\n }\n\n return merged;\n }\n\n private mergeConversationEntries(\n left: PreparedConversationEntry,\n right: PreparedConversationEntry,\n ) {\n const preferred = this.pickPreferredConversationEntry(left, right);\n const other = preferred === left ? right : left;\n const sameIdentifier =\n preferred.conversation.chatIdentifier === other.conversation.chatIdentifier;\n\n return {\n mergeKey: preferred.mergeKey,\n last: preferred.last ?? other.last,\n conversation: {\n ...preferred.conversation,\n displayName: preferred.conversation.displayName ?? other.conversation.displayName,\n participants: [\n ...new Set([...preferred.conversation.participants, ...other.conversation.participants]),\n ],\n unreadCount: sameIdentifier\n ? Math.max(preferred.conversation.unreadCount, other.conversation.unreadCount)\n : preferred.conversation.unreadCount + other.conversation.unreadCount,\n },\n };\n }\n\n private pickPreferredConversationEntry(\n left: PreparedConversationEntry,\n right: PreparedConversationEntry,\n ): PreparedConversationEntry {\n const leftTime = left.conversation.lastMessageDate?.getTime() ?? 0;\n const rightTime = right.conversation.lastMessageDate?.getTime() ?? 0;\n if (leftTime !== rightTime) {\n return leftTime > rightTime ? left : right;\n }\n\n const preferredService = left.last?.lastService ?? right.last?.lastService ?? null;\n if (preferredService) {\n const preferredType = preferredService.toLowerCase().includes(\"sms\") ? \"SMS\" : \"iMessage\";\n if (\n left.conversation.serviceType === preferredType &&\n right.conversation.serviceType !== preferredType\n ) {\n return left;\n }\n if (\n right.conversation.serviceType === preferredType &&\n left.conversation.serviceType !== preferredType\n ) {\n return right;\n }\n }\n\n if (left.conversation.displayName && !right.conversation.displayName) return left;\n if (right.conversation.displayName && !left.conversation.displayName) return right;\n\n if (left.conversation.serviceType === \"iMessage\" && right.conversation.serviceType === \"SMS\") {\n return left;\n }\n if (right.conversation.serviceType === \"iMessage\" && left.conversation.serviceType === \"SMS\") {\n return right;\n }\n\n return left;\n }\n\n private getConversationMergeKey(\n chatIdentifier: string,\n chatGuid: string,\n isGroup: boolean,\n ): string {\n const cacheKey = `${chatIdentifier}::${chatGuid}::${isGroup}`;\n const cached = this.cachedMergeKeys.get(cacheKey);\n if (cached) return cached;\n\n let key: string;\n if (isGroup) {\n key = `group:${chatGuid}`;\n } else {\n const contact = this.contacts.lookupContact(chatIdentifier);\n key = contact\n ? `contact:${contact.contactId}`\n : `identifier:${chatIdentifier.replace(/[\\s\\-()]/g, \"\").toLowerCase()}`;\n }\n\n this.cachedMergeKeys.set(cacheKey, key);\n return key;\n }\n\n private resolveChatsForConversation(identifier: string): ChatRow[] {\n const chats = this.getAllChatsWithLastDate();\n const normalized = identifier.replace(/[\\s\\-()]/g, \"\").toLowerCase();\n\n const directMatches = chats.filter(\n (chat) =>\n chat.chat_identifier === identifier ||\n chat.guid === identifier ||\n (chat.chat_identifier != null &&\n (chat.chat_identifier\n .replace(/[\\s\\-()]/g, \"\")\n .toLowerCase()\n .includes(normalized) ||\n normalized.includes(chat.chat_identifier.replace(/[\\s\\-()]/g, \"\").toLowerCase()))),\n );\n\n if (directMatches.length === 0) return [];\n\n const representative = this.pickMostRecentChat(directMatches);\n const isGroup =\n isGroupGuid(representative.guid) || isGroupChatIdentifier(representative.chat_identifier);\n if (isGroup) return [representative];\n\n const mergeKey = this.getConversationMergeKey(\n representative.chat_identifier,\n representative.guid,\n false,\n );\n\n return chats\n .filter(\n (chat) =>\n !(isGroupGuid(chat.guid) || isGroupChatIdentifier(chat.chat_identifier)) &&\n this.getConversationMergeKey(chat.chat_identifier, chat.guid, false) === mergeKey,\n )\n .map((chat) => toChatRow(chat));\n }\n\n /**\n * Get all chats with their last message date in one join query.\n * Used to resolve the correct chat for a contact when multiple chats exist (e.g. same number).\n * Results are suitable for filtering by handle/identifier then sorting by last_date descending.\n */\n private getAllChatsWithLastDate(): ChatWithLastDate[] {\n const now = Date.now();\n if (this.cachedAllChats && now - this.cachedAllChats.ts < IMessageDB.CACHE_TTL_MS) {\n return this.cachedAllChats.data;\n }\n const stmt = this.raw.prepare(`\n SELECT\n c.ROWID as rowid,\n c.guid,\n c.chat_identifier,\n c.display_name,\n c.service_name,\n (SELECT MAX(m.date)\n FROM ${Tables.CHAT_MESSAGE_JOIN} cmj\n JOIN ${Tables.MESSAGE} m ON cmj.message_id = m.ROWID\n WHERE cmj.chat_id = c.ROWID\n AND m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0) as last_date\n FROM ${Tables.CHAT} c\n `);\n const rows = stmt.all() as {\n rowid: number;\n guid: string;\n chat_identifier: string;\n display_name: string | null;\n service_name: string | null;\n last_date: number | null;\n }[];\n const data = rows.map((r) => ({\n ROWID: r.rowid,\n guid: r.guid,\n chat_identifier: r.chat_identifier,\n display_name: r.display_name,\n service_name: r.service_name,\n last_date: r.last_date,\n }));\n this.cachedAllChats = { data, ts: now };\n return data;\n }\n\n /** Last message metadata per chat (window function over message table). Cached with TTL. */\n private getLastMessageByChat(): Record<number, LastConversationRow> {\n const now = Date.now();\n if (this.cachedLastByChat && now - this.cachedLastByChat.ts < IMessageDB.CACHE_TTL_MS) {\n return this.cachedLastByChat.data;\n }\n const stmt = this.raw.prepare(`\n SELECT chat_id, last_date, last_message_id, last_service, last_is_from_me, balloon_bundle_id, snippet, chat_properties FROM (\n SELECT cmj.chat_id, m.date as last_date, m.ROWID as last_message_id,\n m.service as last_service,\n m.is_from_me as last_is_from_me,\n m.balloon_bundle_id as balloon_bundle_id,\n COALESCE(TRIM(SUBSTR(m.text, 1, 200)), '') as snippet,\n c.properties as chat_properties,\n ROW_NUMBER() OVER (PARTITION BY cmj.chat_id ORDER BY m.date DESC) as rn\n FROM ${Tables.MESSAGE} m\n JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n JOIN ${Tables.CHAT} c ON c.ROWID = cmj.chat_id\n WHERE m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n ) WHERE rn = 1\n `);\n const data = (\n stmt.all() as {\n chat_id: number;\n last_date: number;\n last_message_id: number;\n last_service: string | null;\n last_is_from_me: number;\n balloon_bundle_id: string | null;\n snippet: string;\n chat_properties: Buffer | null;\n }[]\n ).reduce(\n (acc, row) => {\n acc[row.chat_id] = {\n chatId: row.chat_id,\n lastDate: row.last_date,\n lastMessageId: row.last_message_id,\n lastService: row.last_service,\n lastIsFromMe: Boolean(row.last_is_from_me),\n balloonBundleId: row.balloon_bundle_id,\n snippet: row.snippet || null,\n chatProperties: row.chat_properties,\n };\n return acc;\n },\n {} as Record<number, LastConversationRow>,\n );\n this.cachedLastByChat = { data, ts: now };\n return data;\n }\n\n /** Unread count per chat. Cached with TTL. */\n private getUnreadByChat(): Record<number, number> {\n const now = Date.now();\n if (this.cachedUnreadByChat && now - this.cachedUnreadByChat.ts < IMessageDB.CACHE_TTL_MS) {\n return this.cachedUnreadByChat.data;\n }\n const stmt = this.raw.prepare(`\n SELECT cmj.chat_id, COUNT(*) as unread\n FROM ${Tables.MESSAGE} m\n JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE m.associated_message_type = ${AssociatedMessageType.NORMAL}\n AND COALESCE(m.item_type, 0) = 0\n AND m.is_from_me = 0 AND m.is_read = 0\n GROUP BY cmj.chat_id\n `);\n const data = (stmt.all() as { chat_id: number; unread: number }[]).reduce(\n (acc, row) => {\n acc[row.chat_id] = row.unread;\n return acc;\n },\n {} as Record<number, number>,\n );\n this.cachedUnreadByChat = { data, ts: now };\n return data;\n }\n\n /** Get all chats (without last_date subquery -- lighter for listing). */\n private getAllChats(): ChatRow[] {\n const rows = this.raw\n .prepare(\n `SELECT ROWID, guid, chat_identifier, display_name FROM ${Tables.CHAT} ORDER BY ROWID DESC`,\n )\n .all() as ChatRow[];\n return rows;\n }\n\n /**\n * Fetch message rows for a chat ROWID, ordered by date DESC.\n * Replaces the upstream IMessageDatabase.getMessagesFromChat().\n */\n private fetchMessagesForChatRowId(\n chatRowId: number,\n limit: number,\n beforeMessageId?: number,\n afterMessageId?: number,\n ): MessageRow[] {\n if (beforeMessageId != null && afterMessageId != null) {\n // Gap-fill: messages strictly between two boundary IDs\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE cmj.chat_id = ? AND m.ROWID > ? AND m.ROWID < ?\n ORDER BY m.date DESC, m.ROWID DESC\n LIMIT ?\n `);\n return stmt.all(chatRowId, afterMessageId, beforeMessageId, limit) as MessageRow[];\n }\n if (beforeMessageId != null) {\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE cmj.chat_id = ? AND m.ROWID < ?\n ORDER BY m.date DESC, m.ROWID DESC\n LIMIT ?\n `);\n return stmt.all(chatRowId, beforeMessageId, limit) as MessageRow[];\n }\n if (afterMessageId != null) {\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE cmj.chat_id = ? AND m.ROWID > ?\n ORDER BY m.date DESC, m.ROWID DESC\n LIMIT ?\n `);\n return stmt.all(chatRowId, afterMessageId, limit) as MessageRow[];\n }\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID, m.guid, m.text, m.attributedBody, m.date,\n m.is_from_me, h.id as handle_id, m.cache_has_attachments\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE cmj.chat_id = ?\n ORDER BY m.date DESC, m.ROWID DESC\n LIMIT ?\n `);\n return stmt.all(chatRowId, limit) as MessageRow[];\n }\n\n /**\n * Extract readable text from a message row.\n * Prefers the plain text column; falls back to parsing the attributedBody blob.\n * Replaces the upstream IMessageDatabase.parseMessage().\n */\n private parseMessageText(row: MessageRow): string | null {\n if (row.text && !isPlaceholderText(row.text)) return row.text;\n return extractAttributedBodyText(row.attributedBody) || null;\n }\n\n /**\n * From a list of chats with last_date, return the one with the most recent message (sort by last_date desc).\n */\n private pickMostRecentChat(chats: ChatWithLastDate[]): ChatRow {\n if (chats.length === 0) throw new Error(\"pickMostRecentChat requires at least one chat\");\n if (chats.length === 1) return toChatRow(chats[0]);\n const sorted = chats.slice().sort((a, b) => {\n const aDate = a.last_date ?? 0;\n const bDate = b.last_date ?? 0;\n return bDate - aDate;\n });\n return toChatRow(sorted[0]);\n }\n\n /**\n * Helper to find chat by identifier.\n * When multiple chats match (e.g. same number in different services), returns the one with the most recent message.\n */\n private findChatByIdentifier(identifier: string): ChatRow | null {\n const chats = this.getAllChatsWithLastDate();\n const matches = chats.filter(\n (c) =>\n c.chat_identifier === identifier ||\n c.guid === identifier ||\n (c.chat_identifier != null &&\n (c.chat_identifier.includes(identifier) || identifier.includes(c.chat_identifier))),\n );\n if (matches.length === 0) return null;\n return this.pickMostRecentChat(matches);\n }\n\n /**\n * Convert a raw message row to our Message type with full extended data\n */\n private convertMessage(\n raw: MessageRow,\n text: string | null,\n chatId: string,\n extended?: ExtendedMessageData,\n includeReactions: boolean = false,\n ): Message {\n // If no extended data provided, fetch it\n const ext = extended || this.fetchExtendedMessageData(raw.ROWID);\n\n // Clean up text - handle object replacement characters and other special chars\n let cleanText = text || raw.text || null;\n if (isPlaceholderText(cleanText)) {\n const attributedText = extractAttributedBodyText(raw.attributedBody ?? null) || null;\n if (attributedText) {\n cleanText = attributedText;\n }\n }\n if (cleanText) {\n // U+FFFC is Object Replacement Character (inline attachment placeholder); see db-schema\n // U+FFFD is Replacement Character (invalid UTF-8)\n const re = new RegExp(\n `${OBJECT_REPLACEMENT_CHAR.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}+`,\n \"g\",\n );\n cleanText = cleanText\n .replace(re, \"📎 \") // Use paperclip emoji for inline attachments\n .replace(/\\uFFFD/g, \"\")\n .trim();\n\n // If only attachment markers remain, indicate it's an attachment-only message\n if (!cleanText || cleanText === \"📎\" || /^(📎\\s*)+$/.test(cleanText)) {\n cleanText = \"(image/attachment)\";\n }\n\n if (ext.balloon_bundle_id?.includes(\"URLBalloonProvider\")) {\n cleanText = normalizeRichMetadataText(cleanText) ?? cleanText;\n }\n }\n\n // Determine if this is a reaction (see db-schema.ts and docs)\n const associatedType = ext.associated_message_type ?? AssociatedMessageType.NORMAL;\n const isReaction = isReactionType(associatedType);\n\n // Parse reaction info\n let reaction: Reaction | undefined;\n if (isReaction && ext.associated_message_guid) {\n const typeInfo = TAPBACK_TYPE_MAP[associatedType] || {\n type: \"unknown\" as TapbackType,\n isRemoval: false,\n };\n const parsed = parseAssociatedMessageGuid(ext.associated_message_guid);\n\n reaction = {\n type: typeInfo.type,\n emoji: ext.associated_message_emoji || undefined,\n fromHandle: raw.is_from_me ? \"me\" : ext.handle_id || \"unknown\",\n isRemoval: typeInfo.isRemoval,\n targetMessageGuid: parsed?.targetGuid || \"\",\n targetMessagePart: parsed?.partIndex || 0,\n };\n }\n\n // Determine if this is a reply\n const isReply = Boolean(ext.thread_originator_guid);\n let replyTo: ReplyContext | undefined;\n if (isReply && ext.thread_originator_guid) {\n const originalText = this.getMessageTextByGuid(ext.thread_originator_guid);\n replyTo = {\n replyToGuid: ext.thread_originator_guid,\n replyToText: originalText,\n };\n }\n\n // Get rich content type\n const richContentType = getRichContentType(ext.balloon_bundle_id || null);\n\n // Get attachments if needed\n const hasAttachments = Boolean(ext.cache_has_attachments);\n let attachments: Attachment[] | undefined;\n if (hasAttachments) {\n attachments = this.fetchAttachments(raw.ROWID);\n }\n\n // Get reactions for this message if requested\n let reactions: Reaction[] | undefined;\n if (includeReactions && !isReaction) {\n reactions = this.fetchReactionsForMessage(raw.guid);\n // Filter out removal reactions that cancel out adds\n if (reactions.length > 0) {\n reactions = this.consolidateReactions(reactions);\n }\n if (reactions.length === 0) reactions = undefined;\n }\n\n // Resolve display name from contacts\n const rawHandle = raw.is_from_me ? \"me\" : ext.handle_id || \"unknown\";\n const displayName = rawHandle === \"me\" ? undefined : this.contacts.lookupHandle(rawHandle);\n\n // Parse rich content summary if available.\n //\n // `message_summary_info` is set on many plain-text messages too\n // (delivery receipts / typing-indicator metadata, depending on iOS\n // version). The canonical \"this message renders as a rich balloon\"\n // marker is `balloon_bundle_id` — only fall back to parsing the\n // summary blob when that is set, otherwise we paint a misleading\n // \"[Rich Content]\" badge on plain text like \"hello world\".\n let richContentSummary: string | undefined;\n if (ext.balloon_bundle_id && ext.message_summary_info) {\n richContentSummary = this.parseRichContentSummary(ext.message_summary_info);\n }\n\n return {\n id: raw.ROWID,\n guid: raw.guid,\n text: cleanText,\n handle: rawHandle,\n displayName: displayName !== rawHandle ? displayName : undefined,\n isFromMe: Boolean(raw.is_from_me),\n date: macTimestampToDate(raw.date) || new Date(0),\n dateRead: macTimestampToDate(ext.date_read ?? null),\n dateDelivered: macTimestampToDate(ext.date_delivered ?? null),\n isRead: ext.is_read != null ? Boolean(ext.is_read) : true,\n isDelivered: ext.is_delivered != null ? Boolean(ext.is_delivered) : true,\n chatId: chatId,\n service: this.detectServiceForMessage(ext) as \"iMessage\" | \"SMS\",\n isReaction,\n reaction,\n isReply,\n replyTo,\n reactions,\n richContentType,\n richContentSummary,\n isEdited: Boolean(ext.date_edited && ext.date_edited > 0),\n isRetracted: Boolean(ext.date_retracted && ext.date_retracted > 0),\n hasAttachments,\n attachments,\n };\n }\n\n /**\n * Consolidate reactions by removing reactions that are canceled by removal messages\n */\n private consolidateReactions(reactions: Reaction[]): Reaction[] {\n const reactionMap = new Map<string, Reaction>();\n\n for (const r of reactions) {\n const key = `${r.fromHandle}-${r.type}-${r.targetMessagePart}`;\n if (r.isRemoval) {\n reactionMap.delete(key);\n } else {\n reactionMap.set(key, r);\n }\n }\n\n return Array.from(reactionMap.values());\n }\n\n /**\n * Detect service from extended message data (handle.service column).\n */\n private detectServiceForMessage(ext: ExtendedMessageData): \"iMessage\" | \"SMS\" {\n if (ext.handle_service) {\n return ext.handle_service.toLowerCase().includes(\"sms\") ? \"SMS\" : \"iMessage\";\n }\n return \"iMessage\";\n }\n\n /**\n * Schedule a non-blocking background refresh of caches.\n * Call after responding to an MCP tool call or TUI refresh.\n */\n scheduleBackgroundRefresh(): void {\n if (this.backgroundRefreshScheduled) return;\n this.backgroundRefreshScheduled = true;\n setImmediate(() => {\n this.backgroundRefreshScheduled = false;\n try {\n // Invalidate caches so the next request gets fresh data\n this.cachedAllChats = null;\n this.cachedLastByChat = null;\n this.cachedUnreadByChat = null;\n // Pre-warm the cache\n this.getAllChatsWithLastDate();\n // Sync any new slugs\n this.scheduleBackgroundSlugSync();\n } catch {\n // Refresh is best-effort; foreground requests will surface real DB errors.\n }\n });\n }\n\n /** Resolve participant handles to display names via contacts DB. */\n resolveParticipantNames(handles: string[]): string[] {\n return handles.map((h) => this.contacts.lookupHandle(h));\n }\n\n /**\n * Close database connections\n */\n async close(): Promise<void> {\n this.raw.close();\n this.contacts.close();\n this.slugStore.close();\n }\n\n /**\n * Fetch extended message data using raw DB\n * Includes: is_read, date_read, handle_id, reaction data, reply data, edit status, attachments\n */\n private fetchExtendedMessageData(rowid: number): ExtendedMessageData {\n const stmt = this.raw.prepare(`\n SELECT \n m.is_read,\n m.date_read,\n m.is_delivered,\n m.date_delivered,\n h.id as handle_id,\n h.service as handle_service,\n m.associated_message_type,\n m.associated_message_guid,\n m.associated_message_emoji,\n m.thread_originator_guid,\n m.thread_originator_part,\n m.balloon_bundle_id,\n m.item_type,\n m.date_edited,\n m.date_retracted,\n m.cache_has_attachments,\n m.message_summary_info,\n m.payload_data\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n WHERE m.ROWID = ?\n LIMIT 1\n `);\n return (stmt.get(rowid) as ExtendedMessageData) || {};\n }\n\n /**\n * Batch-fetch extended message data for multiple ROWIDs in a single query.\n * Falls back to individual fetches for very large batches (SQLite bind param limit).\n */\n private fetchExtendedMessageDataBatch(rowids: number[]): Map<number, ExtendedMessageData> {\n const result = new Map<number, ExtendedMessageData>();\n if (rowids.length === 0) return result;\n\n const CHUNK = 500;\n for (let i = 0; i < rowids.length; i += CHUNK) {\n const chunk = rowids.slice(i, i + CHUNK);\n const placeholders = chunk.map(() => \"?\").join(\",\");\n const stmt = this.raw.prepare(`\n SELECT\n m.ROWID as _rowid,\n m.is_read, m.date_read, m.is_delivered, m.date_delivered,\n h.id as handle_id, h.service as handle_service,\n m.associated_message_type, m.associated_message_guid,\n m.associated_message_emoji, m.thread_originator_guid,\n m.thread_originator_part, m.balloon_bundle_id,\n m.item_type, m.date_edited, m.date_retracted,\n m.cache_has_attachments, m.message_summary_info, m.payload_data\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n WHERE m.ROWID IN (${placeholders})\n `);\n const rows = stmt.all(...chunk) as (ExtendedMessageData & { _rowid: number })[];\n for (const row of rows) {\n result.set(row._rowid, row);\n }\n }\n return result;\n }\n\n /** Max ROWID currently in the message table — used as a cache key. */\n getMaxMessageRowId(): number {\n const row = this.raw\n .prepare(`SELECT COALESCE(MAX(ROWID), 0) AS m FROM ${Tables.MESSAGE}`)\n .get() as { m: number };\n return Number(row.m) || 0;\n }\n\n /**\n * Fetch every message newer than `cutoffMs` across all chats. Used by the\n * chat_analytics tool — bounded by date, not by per-chat limit. Reactions\n * are included (tapback analytics need them); hidden system items dropped.\n */\n async getMessagesInWindow(cutoffMs: number, capPerWindow = 200_000): Promise<Message[]> {\n const span = perf(\"getMessagesInWindow\");\n const cutoffNanos = Math.floor((cutoffMs / 1000 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);\n const sql = `\n SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.date_read, m.date_delivered,\n m.is_read, m.is_delivered, m.is_from_me, h.id as handle_id,\n h.service as handle_service,\n m.cache_has_attachments, m.associated_message_type,\n m.associated_message_guid, m.associated_message_emoji,\n m.thread_originator_guid, m.thread_originator_part,\n m.balloon_bundle_id, m.message_summary_info,\n m.date_edited, m.date_retracted,\n m.item_type, c.chat_identifier\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n LEFT JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID\n WHERE m.date >= ?\n ORDER BY m.date ASC\n LIMIT ?\n `;\n const rows = this.raw.prepare(sql).all(cutoffNanos, capPerWindow) as any[];\n const out: Message[] = [];\n for (const r of rows) {\n if (isHiddenSystemItem(r.item_type)) continue;\n const text = this.parseMessageText(r);\n // IMPORTANT: ext must carry every field convertMessage uses to\n // classify the row (reaction / edit / reply / service / etc.) —\n // without these, analytics like tapback_summary see every message\n // as a normal text and return 0 reactions.\n const ext: ExtendedMessageData = {\n is_read: r.is_read,\n date_read: r.date_read,\n is_delivered: r.is_delivered,\n date_delivered: r.date_delivered,\n handle_id: r.handle_id,\n handle_service: r.handle_service,\n associated_message_type: r.associated_message_type,\n associated_message_guid: r.associated_message_guid,\n associated_message_emoji: r.associated_message_emoji,\n thread_originator_guid: r.thread_originator_guid,\n thread_originator_part: r.thread_originator_part,\n balloon_bundle_id: r.balloon_bundle_id,\n message_summary_info: r.message_summary_info,\n date_edited: r.date_edited,\n date_retracted: r.date_retracted,\n cache_has_attachments: r.cache_has_attachments,\n item_type: r.item_type,\n };\n const msg = this.convertMessage(r, text, r.chat_identifier || \"\", ext);\n out.push(msg);\n }\n span.end({ cutoffMs, returned: out.length });\n return out;\n }\n\n /**\n * Search attachments by MIME prefix, date window, and/or chat identifier.\n * Excludes stickers (is_sticker=1) and Apple plugin-payload UTIs which\n * aren't meaningful as user-facing attachments.\n *\n * Returns metadata only — use getAttachmentByRowId for the file bytes.\n */\n searchAttachments(opts: {\n mimePrefix?: string;\n chatIdentifier?: string;\n sinceMs?: number;\n untilMs?: number;\n limit: number;\n }): Array<{\n rowId: number;\n filename: string;\n mimeType: string | null;\n transferName: string | null;\n totalBytes: number;\n createdDate: Date;\n chatId: string;\n }> {\n const span = perf(\"searchAttachments\");\n const conds: string[] = [\n \"a.is_sticker = 0\",\n \"(a.uti IS NULL OR a.uti NOT LIKE 'com.apple.messages.plugin%')\",\n ];\n const params: any[] = [];\n if (opts.mimePrefix) {\n conds.push(\"a.mime_type LIKE ?\");\n params.push(`${opts.mimePrefix}%`);\n }\n if (opts.chatIdentifier) {\n conds.push(\"c.chat_identifier = ?\");\n params.push(opts.chatIdentifier);\n }\n const toMacNanos = (ms: number): number =>\n Math.floor((ms / 1000 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);\n if (opts.sinceMs !== undefined) {\n conds.push(\"a.created_date >= ?\");\n params.push(toMacNanos(opts.sinceMs));\n }\n if (opts.untilMs !== undefined) {\n conds.push(\"a.created_date <= ?\");\n params.push(toMacNanos(opts.untilMs));\n }\n const lim = opts.limit > 0 ? opts.limit : 1000;\n params.push(lim);\n\n const sql = `\n SELECT\n a.ROWID as rowId,\n a.filename,\n a.mime_type,\n a.transfer_name,\n a.total_bytes,\n a.created_date,\n c.chat_identifier\n FROM ${Tables.ATTACHMENT} a\n JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id\n JOIN ${Tables.MESSAGE} m ON maj.message_id = m.ROWID\n JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID\n WHERE ${conds.join(\" AND \")}\n ORDER BY a.created_date DESC\n LIMIT ?\n `;\n const rows = this.raw.prepare(sql).all(...params) as any[];\n const out = rows.map((r) => ({\n rowId: Number(r.rowId),\n filename: r.filename || \"\",\n mimeType: r.mime_type ?? null,\n transferName: r.transfer_name ?? null,\n totalBytes: Number(r.total_bytes) || 0,\n // `attachment.created_date` stores seconds since 2001-01-01,\n // not nanoseconds. The auto-detecting converter handles both\n // legacy seconds-formatted rows and any modern ns-formatted rows\n // that may show up in a heterogeneous DB.\n createdDate: macAutoTimestampToDate(Number(r.created_date)) ?? new Date(0),\n chatId: r.chat_identifier || \"\",\n }));\n span.end({ count: out.length });\n return out;\n }\n\n /** Fetch a single attachment record by ROWID. */\n getAttachmentByRowId(rowId: number): {\n rowId: number;\n filename: string;\n mimeType: string | null;\n transferName: string | null;\n totalBytes: number;\n } | null {\n const row = this.raw\n .prepare(\n `SELECT ROWID as rowId, filename, mime_type, transfer_name, total_bytes\n FROM ${Tables.ATTACHMENT}\n WHERE ROWID = ?`,\n )\n .get(rowId) as any;\n if (!row) return null;\n return {\n rowId: Number(row.rowId),\n filename: row.filename || \"\",\n mimeType: row.mime_type ?? null,\n transferName: row.transfer_name ?? null,\n totalBytes: Number(row.total_bytes) || 0,\n };\n }\n\n /**\n * Fetch attachments for a message\n */\n private fetchAttachments(messageRowId: number): Attachment[] {\n const stmt = this.raw.prepare(`\n SELECT \n a.filename,\n a.mime_type,\n a.transfer_name,\n a.total_bytes\n FROM ${Tables.ATTACHMENT} a\n JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id\n WHERE maj.message_id = ?\n `);\n const rows = stmt.all(messageRowId) as any[];\n return rows.map((r) => ({\n filename: r.filename || \"\",\n mimeType: r.mime_type,\n transferName: r.transfer_name,\n totalBytes: r.total_bytes || 0,\n }));\n }\n\n /**\n * Batch-fetch all reactions in a chat, grouped by target message GUID.\n * One query replaces N individual LIKE queries (the main perf bottleneck).\n */\n private fetchReactionsForChat(chatRowId: number): Map<string, Reaction[]> {\n const stmt = this.raw.prepare(`\n SELECT\n m.associated_message_type,\n m.associated_message_guid,\n m.associated_message_emoji,\n h.id as handle_id,\n m.is_from_me\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id\n WHERE cmj.chat_id = ?\n AND m.associated_message_type >= 2000\n `);\n\n const rows = stmt.all(chatRowId) as any[];\n const result = new Map<string, Reaction[]>();\n\n for (const r of rows) {\n const typeInfo = TAPBACK_TYPE_MAP[r.associated_message_type] || {\n type: \"unknown\" as TapbackType,\n isRemoval: false,\n };\n const parsed = parseAssociatedMessageGuid(r.associated_message_guid);\n const targetGuid = parsed?.targetGuid;\n if (!targetGuid) continue;\n\n const reaction: Reaction = {\n type: typeInfo.type,\n emoji: r.associated_message_emoji || undefined,\n fromHandle: r.is_from_me ? \"me\" : r.handle_id || \"unknown\",\n isRemoval: typeInfo.isRemoval,\n targetMessageGuid: targetGuid,\n targetMessagePart: parsed?.partIndex || 0,\n };\n\n const existing = result.get(targetGuid);\n if (existing) existing.push(reaction);\n else result.set(targetGuid, [reaction]);\n }\n\n return result;\n }\n\n /**\n * Fetch reactions for a specific message GUID\n */\n private fetchReactionsForMessage(messageGuid: string): Reaction[] {\n const stmt = this.raw.prepare(`\n SELECT \n m.associated_message_type,\n m.associated_message_guid,\n m.associated_message_emoji,\n h.id as handle_id,\n m.is_from_me\n FROM ${Tables.MESSAGE} m\n LEFT JOIN ${Tables.HANDLE} h ON m.handle_id = h.ROWID\n WHERE m.associated_message_guid LIKE ?\n AND m.associated_message_type >= 2000\n `);\n\n const rows = stmt.all(`%/${messageGuid}`) as any[];\n\n return rows.map((r) => {\n const typeInfo = TAPBACK_TYPE_MAP[r.associated_message_type] || {\n type: \"unknown\" as TapbackType,\n isRemoval: false,\n };\n const parsed = parseAssociatedMessageGuid(r.associated_message_guid);\n\n return {\n type: typeInfo.type,\n emoji: r.associated_message_emoji || undefined,\n fromHandle: r.is_from_me ? \"me\" : r.handle_id || \"unknown\",\n isRemoval: typeInfo.isRemoval,\n targetMessageGuid: parsed?.targetGuid || messageGuid,\n targetMessagePart: parsed?.partIndex || 0,\n };\n });\n }\n\n /**\n * Parse rich content summary from message_summary_info BLOB\n * This contains metadata about link previews, rich messages, etc.\n */\n private parseRichContentSummary(blob: Buffer | null): string | undefined {\n if (!blob) return undefined;\n\n try {\n // message_summary_info is a binary plist - try to extract text\n const str = blob.toString(\"utf8\");\n\n // Look for common patterns in the summary\n // Link URLs\n const urlMatch = str.match(/https?:\\/\\/\\S+/);\n if (urlMatch) {\n return `Link: ${urlMatch[0]}`;\n }\n\n // Title text (often in plists as <string>Title</string>)\n const titleMatch = str.match(/<string>([^<]+)<\\/string>/);\n if (titleMatch) {\n return titleMatch[1];\n }\n\n // For now, just indicate rich content exists\n return \"[Rich Content]\";\n } catch {\n return undefined;\n }\n }\n\n /**\n * Look up the text of a message by GUID (for reply context)\n * Falls back to parsing attributedBody if text is null\n */\n private getMessageTextByRowId(rowId: number): string | null {\n const row = this.raw\n .prepare(`SELECT ROWID, text, attributedBody FROM ${Tables.MESSAGE} WHERE ROWID = ? LIMIT 1`)\n .get(rowId) as\n | { ROWID: number; text: string | null; attributedBody: Buffer | null }\n | undefined;\n return this.extractMessageText(row);\n }\n\n private getMessageTextByGuid(guid: string): string | null {\n const row = this.raw\n .prepare(`SELECT ROWID, text, attributedBody FROM ${Tables.MESSAGE} WHERE guid = ? LIMIT 1`)\n .get(guid) as\n | { ROWID: number; text: string | null; attributedBody: Buffer | null }\n | undefined;\n return this.extractMessageText(row);\n }\n\n private extractMessageText(\n row: { ROWID: number; text: string | null; attributedBody: Buffer | null } | undefined,\n ): string | null {\n if (!row) return null;\n if (row.text && !isPlaceholderText(row.text)) return row.text;\n return extractAttributedBodyText(row.attributedBody) || null;\n }\n}\n\n/**\n * Extended message data from raw database query\n */\ninterface ExtendedMessageData {\n is_read?: number;\n date_read?: number;\n is_delivered?: number;\n date_delivered?: number;\n handle_id?: string | null;\n handle_service?: string | null;\n associated_message_type?: number;\n associated_message_guid?: string | null;\n associated_message_emoji?: string | null;\n thread_originator_guid?: string | null;\n thread_originator_part?: string | null;\n balloon_bundle_id?: string | null;\n item_type?: number;\n date_edited?: number;\n date_retracted?: number;\n cache_has_attachments?: number;\n message_summary_info?: Buffer | null;\n payload_data?: Buffer | null;\n}\n"],"names":["__dirname","require","schemaParseAssociatedMessageGuid","schemaMacTimestampToDate","stmt"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-Bbi1xtl-.js","sources":["../src/tui/exportFormats.ts","../src/types.ts"],"sourcesContent":["/**\n * Format converters for exporting messages from the TUI.\n *\n * All three formats accept the same Message[] + optional header info.\n * Keep these pure (no fs / no React) — the modal + MCP export tool both\n * call into them.\n */\nimport type { Message } from \"../types.js\";\n\nexport interface ExportHeader {\n thread: string;\n participants?: string[];\n serviceType?: string;\n}\n\nfunction fmtDate(d: Date): string {\n return d\n .toISOString()\n .replace(\"T\", \" \")\n .replace(/\\.\\d+Z$/, \"\");\n}\n\nfunction senderLabel(m: Message): string {\n if (m.isFromMe) return \"Me\";\n return m.displayName ?? m.handle;\n}\n\n/** Markdown — human-readable, suitable for sharing or further processing. */\nexport function toMarkdown(messages: Message[], header: ExportHeader): string {\n const lines: string[] = [];\n lines.push(`# ${header.thread}`);\n lines.push(\"\");\n if (header.participants && header.participants.length > 0) {\n lines.push(`**Participants**: ${header.participants.join(\", \")}`);\n }\n if (header.serviceType) {\n lines.push(`**Service**: ${header.serviceType}`);\n }\n lines.push(`**Exported**: ${fmtDate(new Date())} (${messages.length} messages)`);\n lines.push(\"\");\n lines.push(\"---\");\n lines.push(\"\");\n\n for (const m of messages) {\n if (m.isReply && m.replyTo?.replyToText) {\n lines.push(`> ${m.replyTo.replyToText.replace(/\\n/g, \"\\n> \")}`);\n }\n const ts = `[${fmtDate(m.date)}]`;\n const sender = senderLabel(m);\n const text = m.text ?? (m.hasAttachments ? \"*(attachment)*\" : \"*(no text)*\");\n const flags: string[] = [];\n if (m.isEdited) flags.push(\"edited\");\n if (m.hasAttachments) flags.push(\"📎\");\n const flagStr = flags.length > 0 ? ` _(${flags.join(\", \")})_` : \"\";\n lines.push(`**${ts}** ${sender}: ${text}${flagStr}`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n\n/** CSV — for spreadsheet import. RFC 4180-ish quoting. */\nexport function toCSV(messages: Message[]): string {\n const escapeCsv = (v: string | null | undefined): string => {\n if (v == null) return \"\";\n if (/[\",\\n\\r]/.test(v)) {\n return `\"${v.replace(/\"/g, '\"\"')}\"`;\n }\n return v;\n };\n\n const lines: string[] = [];\n lines.push(\n \"id,date,sender,handle,is_from_me,is_read,is_reply,reply_to_text,text,has_attachments\",\n );\n\n for (const m of messages) {\n const row = [\n String(m.id),\n m.date.toISOString(),\n escapeCsv(senderLabel(m)),\n escapeCsv(m.handle),\n m.isFromMe ? \"1\" : \"0\",\n m.isRead ? \"1\" : \"0\",\n m.isReply ? \"1\" : \"0\",\n escapeCsv(m.replyTo?.replyToText ?? \"\"),\n escapeCsv(m.text ?? \"\"),\n m.hasAttachments ? \"1\" : \"0\",\n ];\n lines.push(row.join(\",\"));\n }\n\n return lines.join(\"\\n\");\n}\n\n/** JSON — full message objects, indented, dates as ISO strings. */\nexport function toJSON(messages: Message[], header?: ExportHeader): string {\n const data = {\n ...(header ?? {}),\n exportedAt: new Date().toISOString(),\n count: messages.length,\n messages: messages.map((m) => ({\n ...m,\n date: m.date.toISOString(),\n dateRead: m.dateRead?.toISOString() ?? null,\n dateDelivered: m.dateDelivered?.toISOString() ?? null,\n })),\n };\n return JSON.stringify(data, null, 2);\n}\n\n/** NDJSON — newline-delimited JSON, ideal for streaming exports. */\nexport function toNDJSONLine(m: Message): string {\n return JSON.stringify({\n ...m,\n date: m.date.toISOString(),\n dateRead: m.dateRead?.toISOString() ?? null,\n dateDelivered: m.dateDelivered?.toISOString() ?? null,\n });\n}\n\n/** Choose extension for a given format. */\nexport function extensionFor(format: \"markdown\" | \"csv\" | \"json\" | \"ndjson\"): string {\n switch (format) {\n case \"markdown\":\n return \"md\";\n case \"csv\":\n return \"csv\";\n case \"json\":\n return \"json\";\n case \"ndjson\":\n return \"ndjson\";\n }\n}\n","/**\n * Tapback reaction types\n * 2000-2005: Add reaction, 3000-3005: Remove reaction\n */\nexport type TapbackType =\n | \"love\"\n | \"like\"\n | \"dislike\"\n | \"laugh\"\n | \"emphasize\"\n | \"question\"\n | \"emoji\" // iOS 18+ custom emoji\n | \"sticker\"\n | \"unknown\";\n\n/**\n * Represents a tapback reaction on a message\n */\nexport interface Reaction {\n type: TapbackType;\n emoji?: string; // For custom emoji reactions (iOS 18+)\n fromHandle: string;\n isRemoval: boolean; // true if this removes a previous reaction\n targetMessageGuid: string;\n targetMessagePart: number; // Which part of a multi-part message\n}\n\n/**\n * Represents a reply context\n */\nexport interface ReplyContext {\n replyToGuid: string;\n replyToText?: string | null; // The text of the message being replied to\n}\n\n/**\n * Rich message content types\n */\nexport type RichContentType =\n | \"link_preview\"\n | \"location\"\n | \"digital_touch\"\n | \"handwriting\"\n | \"app_message\"\n | \"unknown\";\n\n/**\n * Represents a message from the iMessage database\n */\nexport interface Message {\n id: number;\n guid: string;\n text: string | null;\n handle: string; // phone number or email\n displayName?: string; // Resolved contact name (if available)\n isFromMe: boolean;\n date: Date;\n dateRead: Date | null;\n dateDelivered: Date | null;\n isRead: boolean;\n isDelivered: boolean;\n chatId: string;\n service: \"iMessage\" | \"SMS\";\n\n // Reaction info (if this message is a tapback)\n isReaction: boolean;\n reaction?: Reaction;\n\n // Reply context (if this message is a reply to another)\n isReply: boolean;\n replyTo?: ReplyContext;\n\n // Reactions received on this message\n reactions?: Reaction[];\n\n // Rich content\n richContentType?: RichContentType;\n richContentSummary?: string; // Parsed summary from message_summary_info BLOB\n\n // Edit/retract status (iOS 16+)\n isEdited: boolean;\n isRetracted: boolean;\n\n // Attachments\n hasAttachments: boolean;\n attachments?: Attachment[];\n}\n\n/**\n * Represents an attachment\n */\nexport interface Attachment {\n filename: string;\n mimeType: string | null;\n transferName: string | null;\n totalBytes: number;\n}\n\n/**\n * Represents a conversation/chat thread\n */\nexport interface Conversation {\n chatId: string;\n chatIdentifier: string; // phone/email or group identifier\n displayName: string | null; // Contact display name or group name\n rawIdentifier: string; // Original phone/email before contact lookup\n participants: string[];\n lastMessageDate: Date | null;\n /** Last message text preview (may be empty if stored in attributedBody only). */\n lastMessageSnippet: string | null;\n unreadCount: number;\n /** Stable human-readable thread slug (e.g. \"alice~imsg~a3f2\"). */\n threadSlug: string;\n /** True for group conversations (multiple participants). */\n isGroupChat: boolean;\n /** Service type for this conversation. */\n serviceType: \"iMessage\" | \"SMS\";\n}\n\n/**\n * Result of sending a message\n */\nexport interface SendMessageResult {\n success: boolean;\n error?: string;\n timestamp?: Date;\n /** Which service actually delivered the message. Populated by reliable-send paths. */\n service?: \"iMessage\" | \"SMS\";\n}\n\n/**\n * Options for waiting for a reply\n */\nexport interface WaitForReplyOptions {\n chatId: string;\n afterMessageId?: number;\n timeoutMs: number;\n pollIntervalMs?: number;\n}\n\n/**\n * Result of waiting for a reply\n */\nexport interface WaitForReplyResult {\n received: boolean;\n message?: Message;\n timedOut: boolean;\n}\n\n/**\n * Conversation thread context for AI agents\n */\nexport interface ConversationThread {\n conversationId: string;\n recipient: string;\n messages: Message[];\n lastSentByMe: Message | null;\n lastReceivedFromThem: Message | null;\n awaitingReply: boolean;\n}\n\n/**\n * Lowest message id in an array — safe for arbitrarily large inputs.\n *\n * `Math.min(...messages.map(m => m.id))` throws\n * \"RangeError: Maximum call stack size exceeded\" past ~125k spread args.\n * Callers shouldn't have to remember that — this helper does the loop.\n * Returns `null` for empty input so the caller can decide on the\n * fallback id explicitly.\n */\nexport function minMessageId(messages: Pick<Message, \"id\">[]): number | null {\n if (messages.length === 0) return null;\n let lo = messages[0].id;\n for (let i = 1; i < messages.length; i++) {\n if (messages[i].id < lo) lo = messages[i].id;\n }\n return lo;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"watchdog-0RGOHpj4.js","sources":["../src/analytics.ts","../src/fuzzy.ts","../src/recipient.ts","../src/watchdog.ts"],"sourcesContent":["/**\n * Chat analytics — pure functions over Message[] arrays.\n *\n * The handler in src/index.ts pulls a bounded message window via\n * IMessageDB.getMessagesInWindow() and dispatches to one of these computeXxx\n * functions based on the analytic `type`. Each returns a structured object\n * the LLM can render or chart.\n *\n * Six priority types are implemented here (P2.5). The remaining 20 from the\n * full enum are listed in mcp-tools.ts as ANALYTICS_TYPES but throw\n * `not_yet_implemented` if requested.\n */\n\nimport type { Message } from \"./types.js\";\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\nconst MS_PER_DAY = 86_400_000;\n\n/** Local-date YYYY-MM-DD for grouping. */\nfunction localDateKey(d: Date): string {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, \"0\");\n const day = String(d.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${day}`;\n}\n\n/** Identifier for \"the other side\" of a message — the chatId so DMs and groups both bucket correctly. */\nfunction contactKey(m: Message): string {\n return m.chatId;\n}\n\n/** Sort helper. */\nfunction byDate(a: Message, b: Message): number {\n return a.date.getTime() - b.date.getTime();\n}\n\n// ── 1. messaging_streaks ─────────────────────────────────────────────────\n\nexport interface StreakResult {\n contact: string;\n longestStreakDays: number;\n longestStreakStart: string | null;\n longestStreakEnd: string | null;\n currentStreakDays: number;\n}\n\nexport function computeStreaks(messages: Message[]): StreakResult[] {\n const byContact = new Map<string, Set<string>>();\n for (const m of messages) {\n if (m.isReaction) continue;\n const c = contactKey(m);\n if (!byContact.has(c)) byContact.set(c, new Set());\n byContact.get(c)!.add(localDateKey(m.date));\n }\n\n const today = localDateKey(new Date());\n const results: StreakResult[] = [];\n\n for (const [contact, dayset] of byContact) {\n const days = Array.from(dayset).sort();\n let longest = 0;\n let longestStart: string | null = null;\n let longestEnd: string | null = null;\n let runLen = 0;\n let runStart: string | null = null;\n\n for (let i = 0; i < days.length; i++) {\n if (\n i === 0 ||\n new Date(days[i]!).getTime() - new Date(days[i - 1]!).getTime() !== MS_PER_DAY\n ) {\n runLen = 1;\n runStart = days[i]!;\n } else {\n runLen++;\n }\n if (runLen > longest) {\n longest = runLen;\n longestStart = runStart;\n longestEnd = days[i]!;\n }\n }\n\n // Current streak — count back from today.\n let curLen = 0;\n const cursor = new Date();\n while (dayset.has(localDateKey(cursor))) {\n curLen++;\n cursor.setDate(cursor.getDate() - 1);\n }\n\n results.push({\n contact,\n longestStreakDays: longest,\n longestStreakStart: longestStart,\n longestStreakEnd: longestEnd,\n currentStreakDays: curLen,\n });\n void today;\n }\n\n results.sort((a, b) => b.longestStreakDays - a.longestStreakDays);\n return results;\n}\n\n// ── 2. double_texts ──────────────────────────────────────────────────────\n\nexport interface DoubleTextResult {\n contact: string;\n doubleTextsFromMe: number;\n doubleTextsFromThem: number;\n}\n\nexport function computeDoubleTexts(messages: Message[]): DoubleTextResult[] {\n const byContact = new Map<string, Message[]>();\n for (const m of messages) {\n if (m.isReaction) continue;\n const c = contactKey(m);\n if (!byContact.has(c)) byContact.set(c, []);\n byContact.get(c)!.push(m);\n }\n\n const results: DoubleTextResult[] = [];\n for (const [contact, msgs] of byContact) {\n msgs.sort(byDate);\n let mineRun = 0;\n let theirsRun = 0;\n let mineDoubles = 0;\n let theirsDoubles = 0;\n for (const m of msgs) {\n if (m.isFromMe) {\n mineRun++;\n theirsRun = 0;\n if (mineRun >= 2) mineDoubles++;\n } else {\n theirsRun++;\n mineRun = 0;\n if (theirsRun >= 2) theirsDoubles++;\n }\n }\n results.push({\n contact,\n doubleTextsFromMe: mineDoubles,\n doubleTextsFromThem: theirsDoubles,\n });\n }\n results.sort(\n (a, b) =>\n b.doubleTextsFromMe + b.doubleTextsFromThem - (a.doubleTextsFromMe + a.doubleTextsFromThem),\n );\n return results;\n}\n\n// ── 3. response_time_stats ───────────────────────────────────────────────\n\nexport interface ResponseTimeStats {\n contact: string;\n count: number;\n medianMs: number;\n p95Ms: number;\n meanMs: number;\n}\n\nfunction percentile(sorted: number[], p: number): number {\n if (sorted.length === 0) return 0;\n const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));\n return sorted[idx] ?? 0;\n}\n\nexport function computeResponseTimes(messages: Message[]): ResponseTimeStats[] {\n const byContact = new Map<string, Message[]>();\n for (const m of messages) {\n if (m.isReaction) continue;\n const c = contactKey(m);\n if (!byContact.has(c)) byContact.set(c, []);\n byContact.get(c)!.push(m);\n }\n const results: ResponseTimeStats[] = [];\n for (const [contact, msgs] of byContact) {\n msgs.sort(byDate);\n const deltas: number[] = [];\n for (let i = 1; i < msgs.length; i++) {\n const prev = msgs[i - 1]!;\n const curr = msgs[i]!;\n // Only count \"me → them\" reply: prev is from them, curr is from me.\n if (!prev.isFromMe && curr.isFromMe) {\n deltas.push(curr.date.getTime() - prev.date.getTime());\n }\n }\n if (deltas.length === 0) continue;\n deltas.sort((a, b) => a - b);\n const mean = deltas.reduce((s, n) => s + n, 0) / deltas.length;\n results.push({\n contact,\n count: deltas.length,\n medianMs: percentile(deltas, 50),\n p95Ms: percentile(deltas, 95),\n meanMs: Math.round(mean),\n });\n }\n results.sort((a, b) => a.medianMs - b.medianMs);\n return results;\n}\n\n// ── 4. daily_heatmap ─────────────────────────────────────────────────────\n\nexport interface HeatmapResult {\n /** 7×24 grid, [dow][hour] = count. dow is 0=Sun..6=Sat. */\n grid: number[][];\n total: number;\n}\n\nexport function computeHeatmap(messages: Message[]): HeatmapResult {\n const grid: number[][] = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));\n let total = 0;\n for (const m of messages) {\n if (m.isReaction) continue;\n const dow = m.date.getDay();\n const hr = m.date.getHours();\n const dowRow = grid[dow];\n if (dowRow && hr >= 0 && hr < 24) {\n dowRow[hr] = (dowRow[hr] ?? 0) + 1;\n total++;\n }\n }\n return { grid, total };\n}\n\n// ── 5. tapback_summary ───────────────────────────────────────────────────\n\nexport interface TapbackResult {\n contact: string;\n heart: number;\n thumbsUp: number;\n thumbsDown: number;\n haha: number;\n exclaim: number;\n question: number;\n emoji: number; // freeform sticker reactions\n total: number;\n}\n\nconst TAPBACK_TYPE = {\n HEART: 2000,\n THUMBS_UP: 2001,\n THUMBS_DOWN: 2002,\n HAHA: 2003,\n EXCLAIM: 2004,\n QUESTION: 2005,\n EMOJI: 2006,\n} as const;\n\nexport function computeTapbacks(messages: Message[]): TapbackResult[] {\n const byContact = new Map<string, TapbackResult>();\n for (const m of messages) {\n if (!m.isReaction || !m.reaction) continue;\n const c = contactKey(m);\n if (!byContact.has(c)) {\n byContact.set(c, {\n contact: c,\n heart: 0,\n thumbsUp: 0,\n thumbsDown: 0,\n haha: 0,\n exclaim: 0,\n question: 0,\n emoji: 0,\n total: 0,\n });\n }\n const r = byContact.get(c)!;\n // m.reaction.type is the textual tag like \"heart\" / \"thumbs_up\" — string\n // mapping rather than the numeric associated_message_type to keep this\n // resilient to type changes.\n const t = (m.reaction.type || \"\").toLowerCase();\n if (t.includes(\"heart\") || t === \"love\") r.heart++;\n else if ((t.includes(\"thumb\") && t.includes(\"up\")) || t === \"like\") r.thumbsUp++;\n else if ((t.includes(\"thumb\") && t.includes(\"down\")) || t === \"dislike\") r.thumbsDown++;\n else if (t.includes(\"haha\") || t === \"laugh\") r.haha++;\n else if (t.includes(\"exclaim\") || t === \"emphasis\") r.exclaim++;\n else if (t.includes(\"question\")) r.question++;\n else r.emoji++;\n r.total++;\n void TAPBACK_TYPE;\n }\n const out = Array.from(byContact.values());\n out.sort((a, b) => b.total - a.total);\n return out;\n}\n\n// ── 6. year_in_review_wrapped ────────────────────────────────────────────\n\nexport interface WrappedResult {\n windowStart: string;\n windowEnd: string;\n totalSent: number;\n totalReceived: number;\n totalReactions: number;\n topContacts: Array<{ contact: string; sent: number; received: number; total: number }>;\n peakDay: { date: string; count: number } | null;\n longestStreakDays: number;\n longestStreakContact: string | null;\n}\n\nexport function computeWrapped(messages: Message[]): WrappedResult {\n if (messages.length === 0) {\n return {\n windowStart: \"\",\n windowEnd: \"\",\n totalSent: 0,\n totalReceived: 0,\n totalReactions: 0,\n topContacts: [],\n peakDay: null,\n longestStreakDays: 0,\n longestStreakContact: null,\n };\n }\n let sent = 0;\n let received = 0;\n let reactions = 0;\n const perContact = new Map<string, { sent: number; received: number; total: number }>();\n const perDay = new Map<string, number>();\n for (const m of messages) {\n if (m.isReaction) {\n reactions++;\n continue;\n }\n if (m.isFromMe) sent++;\n else received++;\n const c = contactKey(m);\n if (!perContact.has(c)) perContact.set(c, { sent: 0, received: 0, total: 0 });\n const pc = perContact.get(c)!;\n if (m.isFromMe) pc.sent++;\n else pc.received++;\n pc.total++;\n const dk = localDateKey(m.date);\n perDay.set(dk, (perDay.get(dk) ?? 0) + 1);\n }\n\n const topContacts = Array.from(perContact.entries())\n .map(([contact, v]) => ({ contact, ...v }))\n .sort((a, b) => b.total - a.total)\n .slice(0, 5);\n\n let peakDay: { date: string; count: number } | null = null;\n for (const [d, count] of perDay) {\n if (!peakDay || count > peakDay.count) peakDay = { date: d, count };\n }\n\n // Longest streak across all contacts (reuse computeStreaks logic).\n const streaks = computeStreaks(messages);\n const top = streaks[0];\n\n return {\n windowStart: localDateKey(messages[0]!.date),\n windowEnd: localDateKey(messages[messages.length - 1]!.date),\n totalSent: sent,\n totalReceived: received,\n totalReactions: reactions,\n topContacts,\n peakDay,\n longestStreakDays: top?.longestStreakDays ?? 0,\n longestStreakContact: top?.contact ?? null,\n };\n}\n\n// ── Dispatch ─────────────────────────────────────────────────────────────\n\nexport type AnalyticType =\n | \"messaging_streaks\"\n | \"double_texts\"\n | \"response_time_stats\"\n | \"daily_heatmap\"\n | \"tapback_summary\"\n | \"year_in_review_wrapped\";\n\n/** Full enum including the 20 deferred types — agents get a friendly error. */\nexport const FUTURE_TYPES = [\n \"silences\",\n \"ghost_storms\",\n \"conversation_half_life\",\n \"sent_received_imbalance\",\n \"tapback_per_person\",\n \"read_receipt_latency\",\n \"most_used_words\",\n \"emoji_leaderboard\",\n \"attachment_volume\",\n \"media_share_breakdown\",\n \"group_chat_activity\",\n \"chat_age_distribution\",\n \"first_messages_log\",\n \"last_messages_log\",\n \"quietest_chats\",\n \"loudest_chats\",\n \"weekend_vs_weekday\",\n \"night_owl_score\",\n \"most_edited_messages\",\n \"retraction_rate\",\n] as const;\n\nexport const IMPLEMENTED_TYPES: AnalyticType[] = [\n \"messaging_streaks\",\n \"double_texts\",\n \"response_time_stats\",\n \"daily_heatmap\",\n \"tapback_summary\",\n \"year_in_review_wrapped\",\n];\n\nexport function dispatchAnalytic(\n type: AnalyticType,\n messages: Message[],\n): {\n type: AnalyticType;\n data: unknown;\n} {\n switch (type) {\n case \"messaging_streaks\":\n return { type, data: computeStreaks(messages) };\n case \"double_texts\":\n return { type, data: computeDoubleTexts(messages) };\n case \"response_time_stats\":\n return { type, data: computeResponseTimes(messages) };\n case \"daily_heatmap\":\n return { type, data: computeHeatmap(messages) };\n case \"tapback_summary\":\n return { type, data: computeTapbacks(messages) };\n case \"year_in_review_wrapped\":\n return { type, data: computeWrapped(messages) };\n }\n}\n","/**\n * Lightweight fuzzy string scoring — a TS port of the \"WRatio\" approach used\n * by thefuzz / fuzzywuzzy. No external dep. Tuned for short, multi-token\n * message-body queries.\n *\n * Returns a normalized score in [0, 1]. Higher = better match.\n *\n * Two-pass:\n * 1. Cheap substring shortcut — if the cleaned query is a substring of the\n * cleaned candidate, return 0.95 immediately (skip the expensive token\n * scoring).\n * 2. Token-set scoring — split both strings into a token set, compute a\n * Sørensen-Dice coefficient over the intersection, then blend with a\n * Levenshtein-based ratio for short queries to catch typos.\n */\n\nconst EMOJI_REGEX = /[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}\\u{1F1E6}-\\u{1F1FF}]/gu;\nconst WHITESPACE_REGEX = /\\s+/g;\n\n/** Lowercase, strip emoji, collapse whitespace, trim. */\nexport function cleanText(s: string): string {\n return s.toLowerCase().replace(EMOJI_REGEX, \" \").replace(WHITESPACE_REGEX, \" \").trim();\n}\n\n/** Tokenize a cleaned string into a Set of unique tokens. */\nfunction tokenSet(s: string): Set<string> {\n if (!s) return new Set();\n return new Set(s.split(\" \").filter(Boolean));\n}\n\n/**\n * Levenshtein distance with early-exit when the running min row exceeds\n * a threshold. We cap the candidate length at 200 chars because past that\n * the cost grows quadratically and the message bodies we care about are\n * typically short.\n */\nfunction levenshtein(a: string, b: string, maxLen = 200): number {\n if (a === b) return 0;\n const aLen = Math.min(a.length, maxLen);\n const bLen = Math.min(b.length, maxLen);\n if (aLen === 0) return bLen;\n if (bLen === 0) return aLen;\n\n let prev = new Array<number>(bLen + 1);\n let curr = new Array<number>(bLen + 1);\n for (let j = 0; j <= bLen; j++) prev[j] = j;\n\n for (let i = 1; i <= aLen; i++) {\n curr[0] = i;\n for (let j = 1; j <= bLen; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n const del = (prev[j] ?? 0) + 1;\n const ins = (curr[j - 1] ?? 0) + 1;\n const sub = (prev[j - 1] ?? 0) + cost;\n curr[j] = Math.min(del, ins, sub);\n }\n [prev, curr] = [curr, prev];\n }\n return prev[bLen] ?? Math.max(aLen, bLen);\n}\n\n/** Levenshtein ratio in [0, 1]: 1 - (distance / max-len). */\nfunction levenshteinRatio(a: string, b: string): number {\n if (a === b) return 1;\n const aLen = Math.min(a.length, 200);\n const bLen = Math.min(b.length, 200);\n const maxLen = Math.max(aLen, bLen);\n if (maxLen === 0) return 1;\n return 1 - levenshtein(a, b) / maxLen;\n}\n\n/**\n * Score query against candidate. Returns 0..1.\n *\n * Performance: O(n + m) substring, O(n*m) Levenshtein worst case (capped at\n * 200 chars). For 10,000-row search this is fine in a single sync sweep.\n */\nexport function fuzzyScore(query: string, candidate: string): number {\n // Raw-substring fast path runs BEFORE cleanText so emoji / punctuation\n // queries still match. `cleanText` strips emoji (good for tokenizing\n // English text, bad if the query *is* an emoji) — without this branch\n // a search for \"🪦\" against \"i need a money 🪦\" returned 0 because\n // cleanText(\"🪦\") === \"\" → score 0 → filtered.\n if (query && candidate && candidate.toLowerCase().includes(query.toLowerCase())) {\n return 0.95;\n }\n\n const q = cleanText(query);\n const c = cleanText(candidate);\n if (!q || !c) return 0;\n\n // 1. Substring shortcut.\n if (c.includes(q)) return 0.95;\n if (q.includes(c)) return 0.9;\n\n // 2. Token-set Dice coefficient.\n const qTokens = tokenSet(q);\n const cTokens = tokenSet(c);\n let intersection = 0;\n for (const tok of qTokens) {\n if (cTokens.has(tok)) intersection++;\n }\n const dice = (2 * intersection) / (qTokens.size + cTokens.size || 1);\n\n // 3. Levenshtein ratio (catches typos in short queries).\n // For single-token queries we ALSO compute lev against the best matching\n // token in the candidate so a typo'd word can still match a longer body.\n let bestTokenLev = 0;\n if (qTokens.size === 1) {\n for (const tok of cTokens) {\n const r = levenshteinRatio(q, tok);\n if (r > bestTokenLev) bestTokenLev = r;\n }\n }\n const levRatio = Math.max(levenshteinRatio(q, c), bestTokenLev);\n\n // Blend: for single-token queries, levRatio dominates (Dice is 0 when\n // tokens don't overlap exactly — a typo kills it). For multi-token\n // queries, token-set is more informative.\n const blend = qTokens.size <= 1 ? 0.1 * dice + 0.9 * levRatio : 0.7 * dice + 0.3 * levRatio;\n\n return Math.max(0, Math.min(1, blend));\n}\n\n/** Score and rank candidates. Returns sorted (best first) with scores. */\nexport function rankFuzzy<T>(\n query: string,\n candidates: T[],\n getText: (c: T) => string,\n minScore = 0.6,\n): Array<{ item: T; score: number }> {\n const out: Array<{ item: T; score: number }> = [];\n for (const c of candidates) {\n const score = fuzzyScore(query, getText(c));\n if (score >= minScore) out.push({ item: c, score });\n }\n out.sort((a, b) => b.score - a.score);\n return out;\n}\n","/**\n * Recipient normalization — parse free-form user input into a handle the\n * send_message layer can act on. Handles four input shapes:\n *\n * 1. E.164 phone +61401990797 → kept as-is\n * 2. Local phone 0401 990 797 → +61401990797 (AU default)\n * 3. iMessage email alice@icloud.com → kept as-is (lowercase)\n * 4. Contact name (typeahead) \"Brian Osborne\" → resolved via ContactsDB\n *\n * Used by:\n * - TUI ComposeRecipientModal (recipient picker stage)\n * - CLI `imsg send <recipient>` (one-off)\n * - MCP `send_message` (agents)\n *\n * Contact-name path returns a `disambiguate` result when multiple contacts\n * match — caller surfaces the contact:N picker (existing P2.7 work).\n */\n\nimport type { Contact } from \"./contacts-db.js\";\n\n/** A normalised, send-ready handle. */\nexport interface ResolvedRecipient {\n kind: \"phone\" | \"email\" | \"contact\";\n /** Handle string send_message accepts (E.164 phone OR email OR chatIdentifier). */\n handle: string;\n /** Display label for confirmation UX. */\n displayName: string;\n}\n\nexport interface AmbiguousRecipient {\n kind: \"ambiguous\";\n query: string;\n candidates: ResolvedRecipient[];\n}\n\nexport interface RecipientError {\n kind: \"error\";\n message: string;\n}\n\nexport type RecipientResolution = ResolvedRecipient | AmbiguousRecipient | RecipientError;\n\n/**\n * Source of contact lookups. Kept narrow so the function is unit-testable\n * without spinning up the full ContactsDB.\n */\nexport interface ContactsSource {\n searchContacts(query: string): Contact[];\n}\n\n/**\n * Read the default country from env (`IMSG_DEFAULT_COUNTRY=AU|US`), falling\n * back to `\"AU\"` (project's primary user is in AU). Used by TUI/CLI/MCP\n * call sites so a single env var changes the default everywhere.\n */\nexport function defaultCountryFromEnv(): \"AU\" | \"US\" {\n const raw = (process.env.IMSG_DEFAULT_COUNTRY ?? \"\").toUpperCase().trim();\n if (raw === \"US\") return \"US\";\n return \"AU\";\n}\n\n/** What we treat as \"phone-like\" before attempting normalization. */\nconst PHONE_LIKE_RE = /^[\\d\\s()+\\-.]+$/;\n\n/** What we treat as \"vanity-phone-like\" — phone-like + letters (e.g. 1-800-FLOWERS). */\nconst VANITY_PHONE_LIKE_RE = /^[\\dA-Za-z\\s()+\\-.]+$/;\n\n/**\n * Phone-keypad letter → digit mapping. ABC=2, DEF=3, …, WXYZ=9.\n * Lowercased input maps the same way.\n */\nconst KEYPAD_MAP: Record<string, string> = {\n a: \"2\",\n b: \"2\",\n c: \"2\",\n d: \"3\",\n e: \"3\",\n f: \"3\",\n g: \"4\",\n h: \"4\",\n i: \"4\",\n j: \"5\",\n k: \"5\",\n l: \"5\",\n m: \"6\",\n n: \"6\",\n o: \"6\",\n p: \"7\",\n q: \"7\",\n r: \"7\",\n s: \"7\",\n t: \"8\",\n u: \"8\",\n v: \"8\",\n w: \"9\",\n x: \"9\",\n y: \"9\",\n z: \"9\",\n};\n\n/**\n * Translate vanity-phone letters to keypad digits in-place. Non-letter chars\n * pass through untouched.\n *\n * \"1-800-FLOWERS\" → \"1-800-3569377\"\n * \"1 800 GO GEICO\" → \"1 800 46 43426\"\n */\nfunction translateVanityLetters(s: string): string {\n let out = \"\";\n for (const ch of s) {\n const lower = ch.toLowerCase();\n out += KEYPAD_MAP[lower] ?? ch;\n }\n return out;\n}\n\n/**\n * Detect a vanity-phone input: phone-like chars + at least one letter +\n * at least one digit. \"Alice\" alone has no digits → not a vanity phone.\n * \"1-800-FLOWERS\" has digits + letters → is.\n */\nfunction isVanityPhone(input: string): boolean {\n if (!VANITY_PHONE_LIKE_RE.test(input)) return false;\n const hasLetter = /[A-Za-z]/.test(input);\n const hasDigit = /\\d/.test(input);\n return hasLetter && hasDigit;\n}\n\n/**\n * Pure helper: turn any user-typed phone number into E.164 format if\n * possible, else return null. Locale: AU default for bare local numbers\n * (the project's primary user is in AU; this is configurable via the\n * `defaultCountry` arg).\n *\n * Examples (defaultCountry=\"AU\"):\n * \"+61401990797\" → \"+61401990797\"\n * \"+1 555 010 0100\" → \"+15550100100\"\n * \"0401 990 797\" → \"+61401990797\"\n * \"(02) 9876 5432\" → \"+61298765432\"\n * \"555-010-0100\" → \"+15550100100\" (when defaultCountry=\"US\")\n * \"abc\" → null\n */\nexport function normalizePhoneToE164(\n input: string,\n defaultCountry: \"AU\" | \"US\" = \"AU\",\n): string | null {\n let trimmed = input.trim();\n if (!trimmed) return null;\n // Vanity-phone pre-pass: \"1-800-FLOWERS\" → \"1-800-3569377\"\n if (isVanityPhone(trimmed)) {\n trimmed = translateVanityLetters(trimmed);\n }\n if (!PHONE_LIKE_RE.test(trimmed)) return null;\n\n const digits = trimmed.replace(/\\D/g, \"\");\n if (digits.length === 0) return null;\n\n // Already E.164-shaped (starts with `+`)? Strip non-digits and re-prepend.\n if (trimmed.startsWith(\"+\")) {\n if (digits.length < 8 || digits.length > 15) return null;\n return `+${digits}`;\n }\n\n // No `+` — interpret based on locale + length.\n if (defaultCountry === \"AU\") {\n // Australian mobiles: 04... (10 digits total)\n if (digits.length === 10 && digits.startsWith(\"0\")) {\n return `+61${digits.slice(1)}`;\n }\n // Australian without leading 0: 4... (9 digits) — accept e.g. \"401990797\"\n if (digits.length === 9 && digits.startsWith(\"4\")) {\n return `+61${digits}`;\n }\n // 11-digit Aussie starting 61: \"61401990797\"\n if (digits.length === 11 && digits.startsWith(\"61\")) {\n return `+${digits}`;\n }\n }\n\n if (defaultCountry === \"US\") {\n // US 10-digit\n if (digits.length === 10) return `+1${digits}`;\n // US 11-digit starting 1\n if (digits.length === 11 && digits.startsWith(\"1\")) return `+${digits}`;\n }\n\n // International-ish but ambiguous: anywhere from 8-15 digits, assume\n // it's already country-coded.\n if (digits.length >= 8 && digits.length <= 15) return `+${digits}`;\n\n return null;\n}\n\n/** Simple email validator — RFC-light, no IDN. */\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport function isLikelyEmail(input: string): boolean {\n return EMAIL_RE.test(input.trim());\n}\n\n/**\n * Resolve a free-form input string into a send-ready recipient (or an\n * ambiguous-contact set / error).\n */\nexport function resolveRecipient(\n input: string,\n ctx: { contacts: ContactsSource; defaultCountry?: \"AU\" | \"US\" },\n): RecipientResolution {\n const trimmed = input.trim();\n if (!trimmed) {\n return { kind: \"error\", message: \"Recipient is required.\" };\n }\n\n // 1. Email\n if (isLikelyEmail(trimmed)) {\n const handle = trimmed.toLowerCase();\n return { kind: \"email\", handle, displayName: handle };\n }\n\n // 2. Phone (E.164, local, or vanity)\n if (PHONE_LIKE_RE.test(trimmed) || isVanityPhone(trimmed)) {\n const e164 = normalizePhoneToE164(trimmed, ctx.defaultCountry ?? \"AU\");\n if (e164) {\n return { kind: \"phone\", handle: e164, displayName: e164 };\n }\n // Phone-shaped but couldn't normalise — fall through to contact search,\n // but don't return phone-error yet (might still be a contact name with\n // digits in it, e.g. \"John 2nd\").\n }\n\n // 3. Contact name typeahead\n const matches = ctx.contacts.searchContacts(trimmed);\n const candidates: ResolvedRecipient[] = [];\n for (const c of matches) {\n // Each contact may have multiple phones and emails. Normalize each\n // phone through the E.164 helper so the resulting handle is dial-ready.\n for (const phone of c.phoneNumbers) {\n const e164 = normalizePhoneToE164(phone, ctx.defaultCountry ?? \"AU\");\n const handle = e164 ?? phone;\n candidates.push({\n kind: \"contact\",\n handle,\n displayName: `${c.displayName} (${handle})`,\n });\n }\n for (const email of c.emails) {\n candidates.push({\n kind: \"email\",\n handle: email.toLowerCase(),\n displayName: `${c.displayName} (${email})`,\n });\n }\n }\n\n if (candidates.length === 0) {\n return {\n kind: \"error\",\n message: `No phone, email, or contact match for \"${trimmed}\".`,\n };\n }\n if (candidates.length === 1) return candidates[0]!;\n return { kind: \"ambiguous\", query: trimmed, candidates };\n}\n","/**\n * Self-healing watchdog.\n *\n * Three independent monitors run on unref'd timers — they never prevent the\n * process from exiting on their own. When any monitor detects an unrecoverable\n * condition it triggers `shutdown()` so the host (Cursor / Claude / Warp)\n * spawns a clean instance.\n *\n * 1. Event-loop lag monitor (perf_hooks.monitorEventLoopDelay)\n * - warn > EVENT_LOOP_WARN_MS p99 over 5s window\n * - kill > EVENT_LOOP_KILL_MS p99 over 5s window\n *\n * 2. Memory monitor\n * - warn > heap exceeds HEAP_WARN_MB (handled by logger.startHeapMonitor)\n * - kill > RSS exceeds MAX_RSS_MB OR heap monotonically grew on\n * MEMORY_GROWTH_SAMPLES consecutive 60s samples\n *\n * 3. Idle / uptime monitor\n * - kill > uptime > IDLE_RESTART_AFTER_MS AND no activity within\n * IDLE_RESTART_QUIET_MS — graceful restart insurance for crufty\n * long-running processes.\n *\n * All thresholds are configurable via env vars so they can be tuned per\n * environment without rebuilding.\n */\n\nimport { writeFileSync } from \"node:fs\";\nimport { type IntervalHistogram, monitorEventLoopDelay } from \"node:perf_hooks\";\nimport { error, info, warn } from \"./logger.js\";\nimport { isShuttingDown, registerCleanup, shutdown } from \"./shutdown.js\";\n\n// ── Config (env-overridable) ─────────────────────────────────────────────\n\nfunction envNum(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nconst EVENT_LOOP_SAMPLE_MS = envNum(\"IMSG_EVENT_LOOP_SAMPLE_MS\", 5_000);\nconst EVENT_LOOP_WARN_MS = envNum(\"IMSG_EVENT_LOOP_WARN_MS\", 500);\nconst EVENT_LOOP_KILL_MS = envNum(\"IMSG_EVENT_LOOP_KILL_MS\", 10_000);\n// Sustained-lag detector: kill if p99 stays >= the sustained threshold for\n// SUSTAINED_SAMPLES consecutive samples. Catches scenarios where the spike\n// kill threshold (10s) is never crossed but the UI is sustained-unusable\n// (e.g. 800ms event-loop lag for several minutes from a render hot-loop).\nconst EVENT_LOOP_SUSTAINED_MS = envNum(\"IMSG_EVENT_LOOP_SUSTAINED_MS\", 750);\nconst EVENT_LOOP_SUSTAINED_SAMPLES = envNum(\"IMSG_EVENT_LOOP_SUSTAINED_SAMPLES\", 6);\n\nconst MEMORY_SAMPLE_MS = envNum(\"IMSG_MEMORY_SAMPLE_MS\", 60_000);\nconst MAX_RSS_MB = envNum(\"IMSG_MAX_RSS_MB\", 1024);\nconst MEMORY_GROWTH_SAMPLES = envNum(\"IMSG_HEAP_GROWTH_SAMPLES\", 10);\n\nconst IDLE_RESTART_AFTER_MS = envNum(\"IMSG_RESTART_AFTER_MS\", 24 * 60 * 60 * 1000); // 24h\nconst IDLE_RESTART_QUIET_MS = envNum(\"IMSG_RESTART_QUIET_MS\", 60 * 60 * 1000); // 1h\nconst IDLE_CHECK_MS = envNum(\"IMSG_IDLE_CHECK_MS\", 10 * 60 * 1000); // 10 min\n\n// ── State ────────────────────────────────────────────────────────────────\n\ninterface WatchdogState {\n startedAt: number;\n eventLoopP99Ms: number;\n eventLoopMaxMs: number;\n /** Consecutive samples where p99 was >= EVENT_LOOP_SUSTAINED_MS. */\n eventLoopSustainedCount: number;\n /** Wall-clock timestamp of the most recent event-loop sample tick.\n * Used to detect system sleep (huge interval gap → reset histogram). */\n lastEventLoopSampleTs: number;\n rssMb: number;\n heapMb: number;\n heapHistory: number[]; // recent heap samples for leak detection\n lastActivityTs: number;\n killReason: string | null;\n}\n\nconst state: WatchdogState = {\n startedAt: Date.now(),\n eventLoopP99Ms: 0,\n eventLoopMaxMs: 0,\n eventLoopSustainedCount: 0,\n lastEventLoopSampleTs: Date.now(),\n rssMb: 0,\n heapMb: 0,\n heapHistory: [],\n lastActivityTs: Date.now(),\n killReason: null,\n};\n\nlet eventLoopHistogram: IntervalHistogram | null = null;\nlet eventLoopTimer: ReturnType<typeof setInterval> | null = null;\nlet memoryTimer: ReturnType<typeof setInterval> | null = null;\nlet idleTimer: ReturnType<typeof setInterval> | null = null;\nlet installed = false;\n\n// ── Public API ───────────────────────────────────────────────────────────\n\n/** Update the activity timestamp — call this from each tool dispatch. */\nexport function noteActivity(): void {\n state.lastActivityTs = Date.now();\n}\n\n/** Read current watchdog state — used by health_check and TUI dev stats. */\nexport function readWatchdogState(): Readonly<WatchdogState> {\n return state;\n}\n\n// ── Memory-pressure subscriber API ───────────────────────────────────────\ntype MemorySampleCallback = (rssMb: number, heapMb: number) => void;\nconst memSampleSubscribers = new Set<MemorySampleCallback>();\n\n/**\n * Subscribe to the watchdog's existing 60s memory sample.\n * Returns an unsubscribe function. Used by the TUI message cache to evict\n * entries under heap pressure without spinning up its own sampler.\n */\nexport function onMemorySample(cb: MemorySampleCallback): () => void {\n memSampleSubscribers.add(cb);\n return () => {\n memSampleSubscribers.delete(cb);\n };\n}\n\nexport interface WatchdogOpts {\n /**\n * Whether to run the 24h idle-restart monitor. Defaults to `true` —\n * correct for the MCP server (host respawns a clean instance after an\n * idle restart).\n *\n * **Set to `false` for the TUI**, where the user IS the parent process\n * and a 24h restart would silently kill an active interactive session\n * (and lose the user's compose state) the moment uptime crosses the\n * threshold. The TUI's lifecycle is \"user runs it until they quit\".\n */\n idleRestart?: boolean;\n}\n\n/** Install all three monitors. Idempotent — safe to call multiple times. */\nexport function installWatchdog(opts: WatchdogOpts = {}): void {\n if (installed) return;\n installed = true;\n const idleRestartEnabled = opts.idleRestart ?? true;\n\n // 1. Event-loop lag monitor\n eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });\n eventLoopHistogram.enable();\n\n const stateFilePath = process.env.IMSG_WATCHDOG_STATE_PATH ?? \"\";\n\n eventLoopTimer = setInterval(() => {\n if (!eventLoopHistogram || isShuttingDown()) return;\n\n // Sleep-skew detection: if wall-clock time between this tick and the\n // previous one is much larger than the sample interval, the laptop\n // probably slept (macOS suspends timers but the histogram keeps\n // accumulating). Reset the histogram and skip threshold evaluation —\n // otherwise we'd kill the process for \"event_loop_blocked\" with p99\n // values like 17 minutes that are pure wall-clock skew, not real lag.\n const now = Date.now();\n const interval = now - state.lastEventLoopSampleTs;\n state.lastEventLoopSampleTs = now;\n if (interval > 3 * EVENT_LOOP_SAMPLE_MS) {\n eventLoopHistogram.reset();\n state.eventLoopSustainedCount = 0;\n info(\"sleep_detected_skipping_sample\", {\n actual_interval_ms: interval,\n expected_interval_ms: EVENT_LOOP_SAMPLE_MS,\n });\n return;\n }\n\n // perf_hooks reports nanoseconds — convert to ms.\n const p99Ms = eventLoopHistogram.percentile(99) / 1e6;\n const maxMs = eventLoopHistogram.max / 1e6;\n state.eventLoopP99Ms = p99Ms;\n state.eventLoopMaxMs = maxMs;\n eventLoopHistogram.reset();\n\n // External observer hook: write state to a JSON file each sample tick so\n // a parent process (e.g. the CI stress harness) can read RSS / lag /\n // sustained-lag-count without parsing logs. Best-effort; failures are\n // silent so the watchdog never crashes the process it's supposed to\n // protect.\n if (stateFilePath) {\n try {\n writeFileSync(\n stateFilePath,\n JSON.stringify({\n ts: Date.now(),\n uptimeMs: Date.now() - state.startedAt,\n eventLoopP99Ms: state.eventLoopP99Ms,\n eventLoopMaxMs: state.eventLoopMaxMs,\n eventLoopSustainedCount: state.eventLoopSustainedCount,\n rssMb: state.rssMb,\n heapMb: state.heapMb,\n killReason: state.killReason,\n }),\n );\n } catch {\n // ignore — non-essential\n }\n }\n\n // Single-spike kill: one sample crossing the spike threshold.\n if (p99Ms >= EVENT_LOOP_KILL_MS) {\n triggerKill(\"event_loop_blocked\", {\n p99_ms: p99Ms,\n max_ms: maxMs,\n threshold_ms: EVENT_LOOP_KILL_MS,\n });\n return;\n }\n\n // Sustained-lag kill: many consecutive samples above the sustained\n // threshold. Catches a render-hot-loop pinning the UI without ever\n // reaching the spike threshold.\n if (p99Ms >= EVENT_LOOP_SUSTAINED_MS) {\n state.eventLoopSustainedCount += 1;\n if (state.eventLoopSustainedCount >= EVENT_LOOP_SUSTAINED_SAMPLES) {\n triggerKill(\"event_loop_sustained_lag\", {\n p99_ms: p99Ms,\n max_ms: maxMs,\n consecutive_samples: state.eventLoopSustainedCount,\n sample_interval_ms: EVENT_LOOP_SAMPLE_MS,\n sustained_threshold_ms: EVENT_LOOP_SUSTAINED_MS,\n });\n return;\n }\n } else {\n state.eventLoopSustainedCount = 0;\n }\n\n if (p99Ms >= EVENT_LOOP_WARN_MS) {\n warn(\"event_loop_lag\", { p99_ms: p99Ms, max_ms: maxMs, threshold_ms: EVENT_LOOP_WARN_MS });\n }\n }, EVENT_LOOP_SAMPLE_MS);\n eventLoopTimer.unref();\n\n // 2. Memory monitor — augments logger.ts heap warnings with hard kill rules\n memoryTimer = setInterval(() => {\n if (isShuttingDown()) return;\n const mu = process.memoryUsage();\n const rssMb = round1(mu.rss / 1024 / 1024);\n const heapMb = round1(mu.heapUsed / 1024 / 1024);\n state.rssMb = rssMb;\n state.heapMb = heapMb;\n\n // Notify subscribers (e.g. TUI message cache) so they can evict on pressure\n for (const cb of memSampleSubscribers) {\n try {\n cb(rssMb, heapMb);\n } catch {\n // Subscriber failures must not crash the watchdog\n }\n }\n\n // Track heap history for monotonic growth detection\n state.heapHistory.push(heapMb);\n if (state.heapHistory.length > MEMORY_GROWTH_SAMPLES) {\n state.heapHistory.shift();\n }\n\n if (rssMb >= MAX_RSS_MB) {\n triggerKill(\"rss_exceeded\", { rss_mb: rssMb, threshold_mb: MAX_RSS_MB });\n return;\n }\n\n if (\n state.heapHistory.length >= MEMORY_GROWTH_SAMPLES &&\n isMonotonicallyGrowing(state.heapHistory)\n ) {\n triggerKill(\"memory_leak_suspected\", {\n samples: state.heapHistory.slice(),\n sample_interval_ms: MEMORY_SAMPLE_MS,\n });\n }\n }, MEMORY_SAMPLE_MS);\n memoryTimer.unref();\n\n // 3. Idle / uptime monitor — kill if uptime > N AND no recent activity.\n // Skipped entirely for the TUI (see WatchdogOpts.idleRestart docs).\n if (idleRestartEnabled) {\n idleTimer = setInterval(() => {\n if (isShuttingDown()) return;\n const uptimeMs = Date.now() - state.startedAt;\n const idleMs = Date.now() - state.lastActivityTs;\n if (uptimeMs >= IDLE_RESTART_AFTER_MS && idleMs >= IDLE_RESTART_QUIET_MS) {\n triggerKill(\"idle_restart\", { uptime_ms: uptimeMs, idle_ms: idleMs });\n }\n }, IDLE_CHECK_MS);\n idleTimer.unref();\n }\n\n registerCleanup(() => {\n if (eventLoopHistogram) {\n eventLoopHistogram.disable();\n eventLoopHistogram = null;\n }\n if (eventLoopTimer) clearInterval(eventLoopTimer);\n if (memoryTimer) clearInterval(memoryTimer);\n if (idleTimer) clearInterval(idleTimer);\n });\n\n info(\"watchdog_installed\", {\n event_loop_warn_ms: EVENT_LOOP_WARN_MS,\n event_loop_kill_ms: EVENT_LOOP_KILL_MS,\n event_loop_sustained_ms: EVENT_LOOP_SUSTAINED_MS,\n event_loop_sustained_samples: EVENT_LOOP_SUSTAINED_SAMPLES,\n max_rss_mb: MAX_RSS_MB,\n memory_growth_samples: MEMORY_GROWTH_SAMPLES,\n idle_restart_after_ms: IDLE_RESTART_AFTER_MS,\n });\n}\n\n// ── Internal helpers ─────────────────────────────────────────────────────\n\nfunction round1(n: number): number {\n return Math.round(n * 10) / 10;\n}\n\n/** Returns true iff every sample is >= the previous (with at least 25MB total growth). */\nexport function isMonotonicallyGrowing(samples: number[]): boolean {\n if (samples.length < 2) return false;\n let prev = samples[0];\n for (let i = 1; i < samples.length; i++) {\n if (samples[i] < prev) return false;\n prev = samples[i];\n }\n // Require at least 25MB total growth to ignore noise. 5MB was too sensitive\n // — innocuous drift triggered false-positive `memory_leak_suspected` kills.\n return samples[samples.length - 1] - samples[0] >= 25;\n}\n\nfunction triggerKill(reason: string, data: Record<string, unknown>): void {\n if (state.killReason) return; // already killing\n state.killReason = reason;\n error(`watchdog_kill: ${reason}`, data);\n // Use shutdown() so registered cleanups run. Force a hard exit if cleanup\n // itself hangs (e.g. SQL is wedged) — 5s grace.\n setTimeout(() => {\n error(\"watchdog_force_exit — graceful shutdown stalled\", { reason });\n process.exit(137);\n }, 5_000).unref();\n shutdown(1).catch(() => process.exit(1));\n}\n"],"names":[],"mappings":"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"}