imsg-mcp 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cli.js +5 -5
- package/dist/{imessage-db-vDZF7E8B.js → imessage-db-Cz8MIIpG.js} +2 -2
- package/dist/{imessage-db-vDZF7E8B.js.map → imessage-db-Cz8MIIpG.js.map} +1 -1
- package/dist/index.js +26 -10
- package/dist/index.js.map +1 -1
- package/dist/{meta-DCZ9Xe9g.js → meta-DzE7CSrZ.js} +2 -2
- package/dist/{meta-DCZ9Xe9g.js.map → meta-DzE7CSrZ.js.map} +1 -1
- package/dist/{setup-B2HejfMT.js → setup-BuXuKAHA.js} +2 -2
- package/dist/{setup-B2HejfMT.js.map → setup-BuXuKAHA.js.map} +1 -1
- package/dist/{shutdown-CikCSZru.js → shutdown-pPAPSnk3.js} +40 -45
- package/dist/shutdown-pPAPSnk3.js.map +1 -0
- package/dist/tui.js +3 -3
- package/dist/{watchdog-CnbBZnfg.js → watchdog-Cbgm050b.js} +2 -2
- package/dist/{watchdog-CnbBZnfg.js.map → watchdog-Cbgm050b.js.map} +1 -1
- package/package.json +1 -1
- package/dist/shutdown-CikCSZru.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog-CnbBZnfg.js","sources":["../src/analytics.ts","../src/fuzzy.ts","../src/recipient.ts","../src/types.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 * 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","/**\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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"}
|
|
1
|
+
{"version":3,"file":"watchdog-Cbgm050b.js","sources":["../src/analytics.ts","../src/fuzzy.ts","../src/recipient.ts","../src/types.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 * 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","/**\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imsg-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "MCP server, CLI, and TUI for reading and sending iMessage / SMS on macOS — lets AI agents text humans, fetch conversation history, and stream-export entire chats. Includes a self-healing watchdog and an optional Rust acceleration module.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"shutdown-CikCSZru.js","sources":["../src/config.ts","../src/logger.ts","../src/db-schema.ts","../src/mock-send-db.ts","../src/applescript.ts","../src/access-check.ts","../src/thread-slug.ts","../src/shutdown.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\nfunction resolveEnvPath(value: string | undefined, fallback: string): string {\n if (!value) return fallback;\n if (value.startsWith(\"~/\") || value === \"~\") {\n return join(homedir(), value.slice(2));\n }\n return resolve(process.cwd(), value);\n}\n\nexport function getEnv(): string {\n return process.env.VITE_ENV ?? \"development\";\n}\n\n/** Bundled DB / mock-sending mode (cloud, CI tests via `.env.test`). */\nexport function isAiEnv(): boolean {\n return getEnv() === \"ai\";\n}\n\n/** Any non-`ai` mode (e.g. `development` on your Mac). */\nexport function isLocalEnv(): boolean {\n return !isAiEnv();\n}\n\nexport function getImsgDbPath(): string {\n return resolveEnvPath(\n process.env.VITE_IMSG_DB_PATH,\n join(homedir(), \"Library\", \"Messages\", \"chat.db\"),\n );\n}\n\nexport function getContactsDbPaths(): string[] | undefined {\n const main =\n process.env.VITE_CONTACTS_DB_PATH ??\n join(homedir(), \"Library\", \"Application Support\", \"AddressBook\", \"AddressBook-v22.abcddb\");\n\n const mainResolved = resolveEnvPath(main, \"\");\n const paths = new Set<string>([mainResolved]);\n\n const sourcesSegment = `${sep}Sources${sep}`;\n const sourcesIndex = mainResolved.indexOf(sourcesSegment);\n const addressBookDir =\n sourcesIndex >= 0 ? mainResolved.slice(0, sourcesIndex) : dirname(mainResolved);\n\n const addSourceDb = (path: string) => {\n if (existsSync(path)) {\n paths.add(path);\n }\n };\n\n const uuid = process.env.VITE_ADDRESS_BOOK_UUID;\n if (uuid) {\n addSourceDb(join(addressBookDir, \"Sources\", uuid, \"AddressBook-v22.abcddb\"));\n }\n\n const sourcesDir = join(addressBookDir, \"Sources\");\n if (existsSync(sourcesDir)) {\n try {\n const entries = readdirSync(sourcesDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n addSourceDb(join(sourcesDir, entry.name, \"AddressBook-v22.abcddb\"));\n }\n } catch {\n // Ignore unreadable Address Book source directories.\n }\n }\n\n return [...paths];\n}\n\nexport function getSlugsDbPath(): string {\n return resolveEnvPath(process.env.VITE_SLUGS_DB_PATH, join(homedir(), \".imsg-mcp\", \"slugs.db\"));\n}\n\nexport function getVcfPath(): string {\n return resolveEnvPath(process.env.VITE_VCF_PATH, join(process.cwd(), \"fixtures\", \"contacts.vcf\"));\n}\n","/**\n * Structured logger with performance monitoring.\n *\n * - In-memory buffer for MCP `get_logs` tool (backwards compatible)\n * - NDJSON file output to $TMPDIR/imsg-mcp/ for post-mortem analysis\n * - Performance spans: `const span = perf(\"op\"); ... span.end({ rows: 100 })`\n * - Heap memory tracking on every log entry\n *\n * Files in $TMPDIR are cleaned up by the OS automatically.\n */\n\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from \"node:fs\";\nimport { freemem, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport type LogLevel = \"info\" | \"warn\" | \"error\" | \"perf\";\n\nexport interface LogEntry {\n ts: string;\n level: LogLevel;\n msg: string;\n dur_ms?: number;\n mem_mb: number;\n mem_delta_mb?: number;\n data?: Record<string, unknown>;\n}\n\nexport interface PerfSpan {\n /** End the span. Logs duration, memory delta, and optional metadata. */\n end(data?: Record<string, unknown>): number;\n}\n\nexport interface LastSendErrorDetails {\n message: string;\n stderr?: string;\n stdout?: string;\n code?: string | number;\n timestamp: string;\n}\n\n// ── State ──────────────────────────────────────────────────────────────\n\nconst MAX_LOG_LINES = 500;\nconst MAX_FILE_BYTES = 10 * 1024 * 1024; // 10 MB rotation\n\n/**\n * File logging is opt-in via IMSG_DEV=1 OR a programmatic `enableFileLogging()`\n * call. End users running the published `imsg mcp` bin don't get NDJSON files\n * in $TMPDIR by default. The TUI flips this on unconditionally so a future\n * crash leaves a postmortem trail — without it, the user's \"tool exited on its\n * own\" report has nothing to investigate. The in-memory ring buffer (500\n * lines) still works either way.\n *\n * Checked at call time, not module-load time, so tests can flip the flag\n * without needing to re-import the module.\n */\nlet fileLoggingForced = false;\n\n/**\n * Force file logging on regardless of `IMSG_DEV`. Called by long-running\n * interactive entry points (TUI) where a postmortem trail justifies the\n * disk usage (file is 10 MB capped). Idempotent. No way to disable once on.\n */\nexport function enableFileLogging(): void {\n fileLoggingForced = true;\n}\n\nfunction isFileLoggingEnabled(): boolean {\n return fileLoggingForced || process.env.IMSG_DEV === \"1\";\n}\n\nfunction isVerboseLogging(): boolean {\n return process.env.IMSG_LOG_VERBOSE === \"1\";\n}\n\nconst memoryLines: string[] = [];\nlet logFilePath: string | null = null;\nlet logFileBytes = 0;\nlet lastSendError: LastSendErrorDetails | null = null;\n\n// ── File output ────────────────────────────────────────────────────────\n\nfunction getLogDir(): string {\n return join(tmpdir(), \"imsg-mcp\");\n}\n\nfunction ensureLogFile(): string | null {\n if (logFilePath && logFileBytes < MAX_FILE_BYTES) return logFilePath;\n\n try {\n const dir = getLogDir();\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\n const date = new Date().toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\n logFilePath = join(dir, `imsg-mcp-${process.pid}-${date}.ndjson`);\n logFileBytes = 0;\n return logFilePath;\n } catch {\n return null;\n }\n}\n\nfunction writeToFile(json: string): void {\n if (!isFileLoggingEnabled()) return;\n const path = ensureLogFile();\n if (!path) return;\n try {\n const line = `${json}\\n`;\n appendFileSync(path, line);\n logFileBytes += line.length;\n } catch {\n // Don't let file I/O failures break the app.\n }\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nfunction heapMB(): number {\n return Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 10) / 10;\n}\n\nfunction formatMemoryLine(entry: LogEntry): string {\n let line = `${entry.ts} [${entry.level}] ${entry.msg}`;\n if (entry.dur_ms != null) line += ` (${entry.dur_ms.toFixed(1)}ms)`;\n if (entry.data != null) line += ` ${JSON.stringify(entry.data)}`;\n return line;\n}\n\nfunction emit(entry: LogEntry): void {\n // In-memory buffer for MCP get_logs tool\n const line = formatMemoryLine(entry);\n memoryLines.push(line);\n if (memoryLines.length > MAX_LOG_LINES) {\n memoryLines.splice(0, memoryLines.length - MAX_LOG_LINES);\n }\n\n // NDJSON file output\n writeToFile(JSON.stringify(entry));\n}\n\n// ── Public API ─────────────────────────────────────────────────────────\n\nexport function info(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"info\", msg, mem_mb: heapMB(), data });\n}\n\nexport function warn(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"warn\", msg, mem_mb: heapMB(), data });\n}\n\nexport function error(msg: string, data?: Record<string, unknown>): void {\n emit({ ts: new Date().toISOString(), level: \"error\", msg, mem_mb: heapMB(), data });\n}\n\n/**\n * Start a performance span. Call `.end()` on the returned object to log\n * the duration, heap delta, and optional metadata.\n *\n * ```ts\n * const span = perf(\"listConversations\");\n * // ... work ...\n * span.end({ chats: 200, deduped: 180 });\n * ```\n */\nexport function perf(msg: string): PerfSpan {\n const startTime = performance.now();\n const startHeap = heapMB();\n\n return {\n end(data?: Record<string, unknown>): number {\n const dur_ms = performance.now() - startTime;\n const endHeap = heapMB();\n emit({\n ts: new Date().toISOString(),\n level: \"perf\",\n msg,\n dur_ms,\n mem_mb: endHeap,\n mem_delta_mb: Math.round((endHeap - startHeap) * 10) / 10,\n data,\n });\n return dur_ms;\n },\n };\n}\n\n// ── Backwards-compatible API (used by MCP tools in index.ts) ───────────\n\n/** @deprecated Use info/warn/error instead. Kept for MCP tool compat. */\nexport function appendLog(level: string, message: string, data?: unknown): void {\n const entry: LogEntry = {\n ts: new Date().toISOString(),\n level: level as LogLevel,\n msg: message,\n mem_mb: heapMB(),\n data:\n data != null\n ? typeof data === \"object\"\n ? (data as Record<string, unknown>)\n : { value: data }\n : undefined,\n };\n emit(entry);\n}\n\nexport function getLogs(tail?: number): string[] {\n if (tail != null && tail > 0) return memoryLines.slice(-tail);\n return [...memoryLines];\n}\n\nexport function clearLogs(): void {\n memoryLines.length = 0;\n}\n\nexport function setLastSendError(details: Omit<LastSendErrorDetails, \"timestamp\">): void {\n lastSendError = { ...details, timestamp: new Date().toISOString() };\n error(\"send_message failed\", details as Record<string, unknown>);\n}\n\nexport function getLastSendError(): LastSendErrorDetails | null {\n return lastSendError ? { ...lastSendError } : null;\n}\n\n/** Return the path to the current log file (if any). */\nexport function getLogFilePath(): string | null {\n return logFilePath;\n}\n\n/** Return the log directory path. */\nexport function getLogDirectory(): string {\n return getLogDir();\n}\n\n/** Log a startup marker — call at process start. */\nexport function logStartup(entrypoint: string): void {\n info(\"startup\", { pid: process.pid, ppid: process.ppid, entrypoint, node: process.version });\n}\n\n/** Log a shutdown marker — call before process exits. */\nexport function logShutdown(reason: string): void {\n info(\"shutdown\", { pid: process.pid, reason, uptime_s: Math.round(process.uptime()) });\n}\n\n/**\n * Read the latest NDJSON log file from disk (for external access).\n * Returns the last N lines from the most recent log file.\n */\nexport function getFileLogLines(tail = 50): string[] {\n try {\n // Previously this used inline `require(\"node:fs\")`, which throws\n // ReferenceError under ESM and gets silently swallowed — returning []\n // even when a 135-line NDJSON sat right next to it. The imports now\n // live at the top of the file (ESM-correct).\n //\n // Strategy: read every PID-tagged file in the log dir so the caller\n // gets logs from THIS server process even when stale files from prior\n // crashes / older PIDs sort later. We fall back to the most-recent\n // file if the current PID isn't present (e.g. file rotated).\n const dir = getLogDir();\n if (!existsSync(dir)) return [];\n const files = readdirSync(dir)\n .filter((f: string) => f.endsWith(\".ndjson\"))\n .sort();\n if (files.length === 0) return [];\n\n const currentPid = String(process.pid);\n const mine = files.filter((f: string) => f.includes(`imsg-mcp-${currentPid}-`));\n const targetFile = mine[mine.length - 1] ?? files[files.length - 1];\n\n const content = readFileSync(join(dir, targetFile), \"utf8\");\n const lines = content.trim().split(\"\\n\").filter(Boolean);\n return tail > 0 ? lines.slice(-tail) : lines;\n } catch {\n return [];\n }\n}\n\n// ── Heap monitor ───────────────────────────────────────────────────────\n\n/**\n * Heap warning threshold for the heartbeat monitor. Tunable via\n * `IMSG_HEAP_WARN_MB` env var. The default of 256 MB is set above the\n * legitimate working-set for big chat_analytics / large search_messages\n * sweeps on real Mac DBs (~240 MB at peak). Pre-fix this was 150,\n * which tripped on every analytic call — agents reading get_logs\n * saw a wall of false-positive \"heap exceeds threshold\" warnings on\n * every healthy invocation.\n */\nconst HEAP_WARN_MB = Math.max(\n 64,\n Number.parseInt(process.env.IMSG_HEAP_WARN_MB ?? \"256\", 10) || 256,\n);\nlet heapMonitorTimer: ReturnType<typeof setInterval> | null = null;\n\n/**\n * Start periodic heap monitoring. Logs a warning if heap exceeds the\n * threshold. Call once at server startup.\n *\n * No-op when IMSG_DEV is unset — end users running the published bin don't\n * accumulate heartbeats. The watchdog itself (src/watchdog.ts) still runs\n * unconditionally for self-healing; this monitor is purely observability.\n *\n * Heartbeats fire every 60s by default, or every 10s when IMSG_LOG_VERBOSE=1.\n */\nexport function startHeapMonitor(): void {\n if (!isFileLoggingEnabled()) return;\n if (heapMonitorTimer) return;\n const intervalMs = isVerboseLogging() ? 10_000 : 60_000;\n heapMonitorTimer = setInterval(() => {\n const heap = heapMB();\n const { rss } = process.memoryUsage();\n const rssMb = Math.round((rss / 1024 / 1024) * 10) / 10;\n if (heap > HEAP_WARN_MB) {\n warn(\"heap exceeds threshold\", { heap_mb: heap, rss_mb: rssMb, threshold_mb: HEAP_WARN_MB });\n }\n // System-level memory pressure — captures cases where the host or OS may\n // be about to reclaim us. Helps diagnose \"process vanished\" reports where\n // the kill came from outside (SIGKILL, OOM, parent host).\n const freeMb = Math.round(freemem() / 1024 / 1024);\n // Always log a periodic heartbeat at info level for post-mortem analysis\n emit({\n ts: new Date().toISOString(),\n level: \"info\",\n msg: \"heartbeat\",\n mem_mb: heap,\n data: {\n rss_mb: rssMb,\n uptime_s: Math.round(process.uptime()),\n system_free_mb: freeMb || undefined,\n },\n });\n }, intervalMs);\n // Don't prevent process exit\n heapMonitorTimer.unref();\n}\n\nexport function stopHeapMonitor(): void {\n if (heapMonitorTimer) {\n clearInterval(heapMonitorTimer);\n heapMonitorTimer = null;\n }\n}\n","/**\n * iMessage SQLite database schema constants and helpers.\n * Single source of truth for chat.db structure (synced with Messages.app / SMS).\n * See docs/IMESSAGE_DB_SCHEMA.md for full reference.\n */\n\n/** Seconds between Unix epoch (1970-01-01) and Mac epoch (2001-01-01). */\nexport const MAC_EPOCH_OFFSET = 978307200;\n\n/** Timestamps in DB are nanoseconds since 2001-01-01. */\nexport const NANOS_PER_SECOND = 1_000_000_000;\n\nexport const Tables = {\n MESSAGE: \"message\",\n HANDLE: \"handle\",\n CHAT: \"chat\",\n CHAT_MESSAGE_JOIN: \"chat_message_join\",\n CHAT_HANDLE_JOIN: \"chat_handle_join\",\n ATTACHMENT: \"attachment\",\n MESSAGE_ATTACHMENT_JOIN: \"message_attachment_join\",\n RECOVERABLE_MESSAGE_PART: \"recoverable_message_part\",\n} as const;\n\n/** associated_message_type: 0 = normal message; 2,3 = business; 1000 = sticker; 2000–3006 = tapbacks. */\nexport const AssociatedMessageType = {\n NORMAL: 0,\n STICKER: 1000,\n LOVE_ADD: 2000,\n LIKE_ADD: 2001,\n DISLIKE_ADD: 2002,\n LAUGH_ADD: 2003,\n EMPHASIZE_ADD: 2004,\n QUESTION_ADD: 2005,\n EMOJI_ADD: 2006,\n LOVE_REMOVE: 3000,\n LIKE_REMOVE: 3001,\n DISLIKE_REMOVE: 3002,\n LAUGH_REMOVE: 3003,\n EMPHASIZE_REMOVE: 3004,\n QUESTION_REMOVE: 3005,\n EMOJI_REMOVE: 3006,\n} as const;\n\n/** Minimum type value for tapback/reaction messages (add). */\nexport const TAPBACK_ADD_MIN = 2000;\n\n/** Minimum type value for reaction messages we query (adds). */\nexport const TAPBACK_QUERY_MIN = 2000;\n\n/** Format: p:PART_INDEX/MESSAGE_GUID. */\nexport const ASSOCIATED_MESSAGE_GUID_REGEX = /^p:(\\d+)\\/(.+)$/;\n\n/** Object replacement character used as placeholder for inline attachments / rich content. */\nexport const OBJECT_REPLACEMENT_CHAR = \"\\uFFFC\";\n\n/**\n * Convert macOS timestamp (nanoseconds since 2001-01-01) to JS Date.\n */\nexport function macTimestampToDate(timestamp: number | null): Date | null {\n if (timestamp == null || timestamp === 0) return null;\n const unixSeconds = timestamp / NANOS_PER_SECOND + MAC_EPOCH_OFFSET;\n return new Date(unixSeconds * 1000);\n}\n\n/**\n * Convert a macOS timestamp where the storage unit isn't guaranteed.\n *\n * `message.date` is nanoseconds on modern macOS, but other columns\n * (notably `attachment.created_date`) store **seconds** since the\n * Mac epoch. Same baseline, different magnitude — and the two ranges\n * don't overlap for any reasonable date:\n * - \"now\" in seconds: ~8 × 10⁸\n * - \"now\" in nanoseconds: ~8 × 10¹⁷\n *\n * Anything below `SECONDS_NS_BOUNDARY` (10¹⁵) is treated as seconds;\n * anything at or above is treated as nanoseconds. Values of 0 or null\n * return null (no recorded timestamp).\n */\nexport const SECONDS_NS_BOUNDARY = 1e15;\n\nexport function macAutoTimestampToDate(timestamp: number | null): Date | null {\n if (timestamp == null || timestamp === 0) return null;\n const unitDivisor = timestamp < SECONDS_NS_BOUNDARY ? 1 : NANOS_PER_SECOND;\n const unixSeconds = timestamp / unitDivisor + MAC_EPOCH_OFFSET;\n return new Date(unixSeconds * 1000);\n}\n\n/**\n * Parse associated_message_guid to extract target message GUID and part index.\n * Format: \"p:PART_INDEX/MESSAGE_GUID\"\n */\nexport function parseAssociatedMessageGuid(\n guid: string | null,\n): { targetGuid: string; partIndex: number } | null {\n if (!guid) return null;\n const match = guid.match(ASSOCIATED_MESSAGE_GUID_REGEX);\n if (match) {\n return { partIndex: parseInt(match[1], 10), targetGuid: match[2] };\n }\n return null;\n}\n\n/** Whether associated_message_type represents a reaction/tapback (excludes normal and business). */\nexport function isReactionType(associatedMessageType: number): boolean {\n return associatedMessageType >= 1000 && associatedMessageType !== 0;\n}\n","import { randomUUID } from \"node:crypto\";\nimport Database from \"better-sqlite3\";\nimport { MAC_EPOCH_OFFSET, NANOS_PER_SECOND } from \"./db-schema.js\";\n\nfunction dateToMacTimestamp(date: Date): number {\n return Math.floor((date.getTime() / 1000 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);\n}\n\nfunction resolveChat(\n db: Database.Database,\n opts: { chatIdentifier?: string; chatGuid?: string },\n): { chatRowId: number; handleId: number } | null {\n let chatRow: any;\n if (opts.chatGuid) {\n chatRow = db\n .prepare(\"SELECT ROWID, chat_identifier FROM chat WHERE guid = ?\")\n .get(opts.chatGuid);\n }\n if (!chatRow && opts.chatIdentifier) {\n chatRow = db\n .prepare(\"SELECT ROWID, chat_identifier FROM chat WHERE chat_identifier = ?\")\n .get(opts.chatIdentifier);\n }\n if (!chatRow) return null;\n\n const handle: any = db\n .prepare(\"SELECT ROWID FROM handle WHERE id = ?\")\n .get(chatRow.chat_identifier);\n\n const handleId = handle?.ROWID ?? 0;\n return { chatRowId: chatRow.ROWID, handleId };\n}\n\nexport function insertSentMessage(\n dbPath: string,\n target: { chatIdentifier?: string; chatGuid?: string },\n text: string,\n): number | null {\n const db = new Database(dbPath);\n db.pragma(\"journal_mode = WAL\");\n\n try {\n const resolved = resolveChat(db, target);\n if (!resolved) {\n console.warn(\"[mock-send] Could not resolve chat for\", target);\n return null;\n }\n\n const now = new Date();\n const macDate = dateToMacTimestamp(now);\n const guid = `mock:${randomUUID()}`;\n\n const insert = db.prepare(`\n INSERT INTO message (guid, text, handle_id, date, is_from_me, associated_message_type, is_read, is_delivered, service)\n VALUES (?, ?, ?, ?, 1, 0, 1, 1, 'iMessage')\n `);\n const result = insert.run(guid, text, resolved.handleId, macDate);\n const messageRowId = Number(result.lastInsertRowid);\n\n db.prepare(\n \"INSERT INTO chat_message_join (chat_id, message_id, message_date) VALUES (?, ?, ?)\",\n ).run(resolved.chatRowId, messageRowId, macDate);\n\n return messageRowId;\n } finally {\n db.close();\n }\n}\n","import { execFile } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { getImsgDbPath, isAiEnv } from \"./config.js\";\nimport { setLastSendError } from \"./logger.js\";\nimport { insertSentMessage } from \"./mock-send-db.js\";\nimport type { SendMessageResult } from \"./types.js\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Detect whether a handle looks like a phone number suitable for SMS\n * fallback. We don't bother validating the country/area code — Messages.app\n * will reject malformed ones with a clear error.\n */\nfunction isPhoneLike(handle: string): boolean {\n return /^\\+?[\\d\\s\\-()]+$/.test(handle.trim());\n}\n\n/** Result shape for the preflight reachability check. */\nexport interface ImessageAvailability {\n /** Best-guess service that will reach this handle. */\n service: \"iMessage\" | \"SMS\" | \"unknown\";\n /** True iff at least one service appears to be reachable for the handle. */\n reachable: boolean;\n /** Human-readable hint when not reachable, for LLM remediation. */\n hint?: string;\n}\n/** `VITE_ENV=ai`, or any test run (never hit Messages.app / osascript under Vitest). */\nconst MOCK = isAiEnv() || process.env.VITEST === \"true\";\n\n// ---------------------------------------------------------------------------\n// Mock helpers (MOCK: return success + optional insert into chat.db)\n// ---------------------------------------------------------------------------\n\nfunction mockSend(\n text: string,\n target: { chatIdentifier?: string; chatGuid?: string },\n): SendMessageResult {\n // Under Vitest, return success only — no SQLite (avoids LFS/pointer noise; tests cover API shape).\n if (process.env.VITEST !== \"true\") {\n try {\n insertSentMessage(getImsgDbPath(), target, text);\n } catch (err) {\n console.warn(\"[mock-send] DB insert failed (non-fatal):\", err);\n }\n }\n return { success: true, timestamp: new Date() };\n}\n\n// ---------------------------------------------------------------------------\n// Real AppleScript helpers\n// ---------------------------------------------------------------------------\n\nasync function runAppleScript(\n script: string,\n captureErrorForSend: boolean = false,\n): Promise<string> {\n try {\n const { stdout, stderr } = await execFileAsync(\"osascript\", [\"-e\", script], {\n timeout: 30000,\n });\n if (stderr?.trim()) {\n console.error(\"[osascript] stderr:\", stderr);\n }\n return stdout.trim();\n } catch (error: any) {\n if (captureErrorForSend) {\n setLastSendError({\n message: error.message || String(error),\n stderr: error.stderr ?? undefined,\n stdout: error.stdout ?? undefined,\n code: error.code ?? undefined,\n });\n }\n if (error.code === \"ENOENT\") {\n throw new Error(\"osascript not found. This tool requires macOS.\");\n }\n if (error.stderr) {\n throw new Error(`AppleScript error: ${error.stderr}`);\n }\n throw error;\n }\n}\n\nexport function appleScriptEscape(str: string): string {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n}\n\n// ---------------------------------------------------------------------------\n// Public API -- each function branches on MOCK\n// ---------------------------------------------------------------------------\n\nexport async function sendMessage(recipient: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n set targetService to 1st account whose service type = iMessage\n set targetBuddy to participant \"${escapedRecipient}\" of targetService\n send \"${escapedMessage}\" to targetBuddy\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendMessageAlt(\n recipient: string,\n message: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to buddy \"${escapedRecipient}\" of (service 1 whose service type is iMessage)\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (_error: any) {\n return sendSMS(recipient, message);\n }\n}\n\nexport async function sendSMS(phoneNumber: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: phoneNumber });\n\n const escapedPhone = appleScriptEscape(phoneNumber);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to buddy \"${escapedPhone}\" of (service 1 whose service type is SMS)\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendToChat(chatName: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: chatName });\n\n const escapedName = appleScriptEscape(chatName);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to chat \"${escapedName}\"\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function sendToChatId(chatId: string, message: string): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatGuid: chatId });\n\n const escapedId = appleScriptEscape(chatId);\n const escapedMessage = appleScriptEscape(message);\n const script = `\n tell application \"Messages\"\n send \"${escapedMessage}\" to text chat id \"${escapedId}\"\n end tell\n `;\n try {\n await runAppleScript(script, true);\n return { success: true, timestamp: new Date() };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\nexport async function checkMessagesAvailable(): Promise<boolean> {\n if (MOCK) return true;\n\n const script = `\n tell application \"System Events\"\n return exists application process \"Messages\"\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result === \"true\";\n } catch {\n return false;\n }\n}\n\nexport async function getAvailableServices(): Promise<string[]> {\n if (MOCK) return [\"iMessage\", \"SMS\"];\n\n const script = `\n tell application \"Messages\"\n set serviceList to {}\n repeat with s in services\n set end of serviceList to (service type of s as string)\n end repeat\n return serviceList\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result\n .split(\", \")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nexport async function activateMessages(): Promise<void> {\n if (MOCK) return;\n await runAppleScript('tell application \"Messages\" to activate');\n}\n\nexport async function buddyExists(address: string): Promise<boolean> {\n if (MOCK) return true;\n\n const escaped = appleScriptEscape(address);\n const script = `\n tell application \"Messages\"\n try\n set targetBuddy to buddy \"${escaped}\" of (service 1 whose service type is iMessage)\n return true\n on error\n return false\n end try\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n return result === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Reliable send: writes the message body to a UTF-8 temp file and uses\n * `read (POSIX file \"...\") as «class utf8»` inside AppleScript. This avoids\n * two recurring failure modes of inline-string sends:\n * 1. AppleScript string-length limits on very long messages.\n * 2. Quote/backslash/escape bugs when the message contains arbitrary user\n * content (emoji ZWJ sequences, smart quotes, backticks, etc).\n *\n * Auto-fallback: if iMessage send fails AND the recipient looks like a phone\n * number, retries via the SMS service in the same AppleScript invocation.\n */\nexport async function sendMessageReliable(\n recipient: string,\n message: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(message, { chatIdentifier: recipient });\n\n const tmpFile = join(tmpdir(), `imsg-send-${randomBytes(8).toString(\"hex\")}.txt`);\n try {\n writeFileSync(tmpFile, message, { encoding: \"utf8\" });\n } catch (error: any) {\n return { success: false, error: `Failed to stage send payload: ${error.message ?? error}` };\n }\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedPath = appleScriptEscape(tmpFile);\n const phoneFallback = isPhoneLike(recipient);\n\n // Outer try sends via iMessage; if it errors AND the recipient is a phone\n // number, the on-error branch falls back to SMS. Both branches read the\n // body from the temp file to avoid AppleScript string handling.\n const script = phoneFallback\n ? `\n tell application \"Messages\"\n set msgBody to read (POSIX file \"${escapedPath}\") as «class utf8»\n try\n set iSvc to 1st account whose service type = iMessage\n send msgBody to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n on error\n set sSvc to 1st account whose service type = SMS\n send msgBody to participant \"${escapedRecipient}\" of sSvc\n return \"SMS\"\n end try\n end tell\n `\n : `\n tell application \"Messages\"\n set msgBody to read (POSIX file \"${escapedPath}\") as «class utf8»\n set iSvc to 1st account whose service type = iMessage\n send msgBody to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n end tell\n `;\n\n try {\n const service = await runAppleScript(script, true);\n return {\n success: true,\n timestamp: new Date(),\n service: service === \"SMS\" || service === \"iMessage\" ? service : undefined,\n };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n } finally {\n // Best-effort cleanup — failure here is non-fatal (OS will reap $TMPDIR).\n try {\n unlinkSync(tmpFile);\n } catch {\n // ignore\n }\n }\n}\n\n/**\n * Send a file attachment to a participant. Returns success/error.\n *\n * Uses `send (POSIX file \"...\") to participant ...` — the Messages SDEF\n * accepts a file specifier as the first argument to `send`. The file must\n * exist on disk; Messages.app will rate-limit or reject very large files\n * (typical cap ~100MB).\n *\n * For phone-like recipients, falls back to SMS service on iMessage failure.\n */\nexport async function sendAttachment(\n recipient: string,\n filepath: string,\n): Promise<SendMessageResult> {\n if (MOCK) return mockSend(`[attachment:${filepath}]`, { chatIdentifier: recipient });\n\n const escapedRecipient = appleScriptEscape(recipient);\n const escapedPath = appleScriptEscape(filepath);\n const phoneFallback = isPhoneLike(recipient);\n\n const script = phoneFallback\n ? `\n tell application \"Messages\"\n try\n set iSvc to 1st account whose service type = iMessage\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n on error\n set sSvc to 1st account whose service type = SMS\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of sSvc\n return \"SMS\"\n end try\n end tell\n `\n : `\n tell application \"Messages\"\n set iSvc to 1st account whose service type = iMessage\n send (POSIX file \"${escapedPath}\") to participant \"${escapedRecipient}\" of iSvc\n return \"iMessage\"\n end tell\n `;\n\n try {\n const service = await runAppleScript(script, true);\n return {\n success: true,\n timestamp: new Date(),\n service: service === \"SMS\" || service === \"iMessage\" ? service : undefined,\n };\n } catch (error: any) {\n return { success: false, error: error.message || String(error) };\n }\n}\n\n/**\n * Preflight reachability check. Cheap call that an agent should make BEFORE\n * `send_message` to avoid wasted attempts when the handle can't be reached.\n *\n * Detection rules:\n * - Try iMessage `buddy` lookup → if success, return `iMessage`.\n * - If handle looks like a phone number, try SMS `buddy` → if success,\n * return `SMS`.\n * - Otherwise return `unknown` with a remediation hint.\n */\n/**\n * Cheap format check used by checkImessageAvailability before invoking\n * AppleScript. Messages.app's `buddy \"...\" of account` resolution is\n * lazy: any string returns a buddy reference, even literal nonsense\n * (\"not-a-handle\"). Without this guard the preflight reports\n * reachable:true for garbage and the caller's send_message later fails.\n *\n * Accepts E.164 phones (and digit/separator variants — the broader\n * recipient normalizer in `recipient.ts` will pick those up downstream)\n * and properly-shaped emails. Returns null when the handle passes; the\n * caller should then run the AppleScript probe.\n */\nexport function validateAvailabilityHandle(handle: string): ImessageAvailability | null {\n const trimmed = handle.trim();\n const isEmail = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(trimmed);\n const isPhone = /^\\+\\d{6,15}$/.test(trimmed.replace(/[\\s()\\-.]/g, \"\"));\n if (isEmail || isPhone) return null;\n return {\n service: \"unknown\",\n reachable: false,\n hint: \"Handle is neither a valid email nor a recognizable phone (expect E.164 like +61401990797).\",\n };\n}\n\nexport async function checkImessageAvailability(handle: string): Promise<ImessageAvailability> {\n // Reject obviously-invalid input first so we don't get false-positive\n // \"reachable: true\" from AppleScript's lazy buddy resolution.\n const formatFail = validateAvailabilityHandle(handle);\n if (formatFail) return formatFail;\n\n if (MOCK) {\n return { service: \"iMessage\", reachable: true };\n }\n\n const escaped = appleScriptEscape(handle);\n const script = `\n tell application \"Messages\"\n try\n set b to buddy \"${escaped}\" of (1st account whose service type is iMessage)\n return \"iMessage\"\n on error\n try\n set b to buddy \"${escaped}\" of (1st account whose service type is SMS)\n return \"SMS\"\n on error\n return \"unknown\"\n end try\n end try\n end tell\n `;\n try {\n const result = await runAppleScript(script);\n if (result === \"iMessage\" || result === \"SMS\") {\n return { service: result, reachable: true };\n }\n return {\n service: \"unknown\",\n reachable: false,\n hint: isPhoneLike(handle)\n ? \"Handle not found in iMessage or SMS buddies. Verify the number format (try '+1...' for US) and that the recipient has at least one of iMessage or SMS reachable.\"\n : \"Handle not found in iMessage buddies. For email addresses, the recipient must have iMessage active on that address.\",\n };\n } catch (error: any) {\n return {\n service: \"unknown\",\n reachable: false,\n hint: `Availability check failed: ${error.message ?? error}. Most common cause: Messages.app Automation permission is not granted to this terminal/IDE.`,\n };\n }\n}\n","import { existsSync } from \"node:fs\";\nimport os from \"node:os\";\nimport Database from \"better-sqlite3\";\nimport { checkMessagesAvailable } from \"./applescript.js\";\nimport { getContactsDbPaths, getImsgDbPath } from \"./config.js\";\n\nexport type AccessStatus = \"ok\" | \"warn\" | \"error\";\n\nexport interface AccessCheckItem {\n key: string;\n label: string;\n status: AccessStatus;\n detail: string;\n}\n\nexport interface AccessReport {\n ok: boolean;\n items: AccessCheckItem[];\n}\n\nfunction inspectSqlite(path: string): string | null {\n const db = new Database(path, { readonly: true, fileMustExist: true });\n try {\n db.pragma(\"schema_version\");\n return null;\n } finally {\n db.close();\n }\n}\n\nfunction classifyDbError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n if (/operation not permitted/i.test(message) || /authorization denied/i.test(message)) {\n return [\n \"Full Disk Access is missing for the app running this command.\",\n \"Grant Full Disk Access to Terminal, iTerm2, Warp, VS Code, Cursor, or whichever app is launching imsg-mcp, then fully restart that app.\",\n \"Open System Settings -> Privacy & Security -> Full Disk Access.\",\n ].join(\" \");\n }\n if (/unable to open database file/i.test(message) || /no such file/i.test(message)) {\n return \"Database file was not found at the configured path.\";\n }\n return message;\n}\n\nfunction iconFor(status: AccessStatus): string {\n if (status === \"ok\") return \"OK\";\n if (status === \"warn\") return \"WARN\";\n return \"ERR\";\n}\n\nexport async function checkLocalAccess(): Promise<AccessReport> {\n const items: AccessCheckItem[] = [];\n\n items.push({\n key: \"platform\",\n label: \"Platform\",\n status: process.platform === \"darwin\" ? \"ok\" : \"error\",\n detail:\n process.platform === \"darwin\"\n ? `Running on macOS ${os.release()}`\n : `Running on ${process.platform}; live iMessage access only works on macOS.`,\n });\n\n const imsgDbPath = getImsgDbPath();\n if (!existsSync(imsgDbPath)) {\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: \"error\",\n detail: `Missing ${imsgDbPath}`,\n });\n } else {\n try {\n const error = inspectSqlite(imsgDbPath);\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: error ? \"error\" : \"ok\",\n detail: error ? classifyDbError(error) : `Readable at ${imsgDbPath}`,\n });\n } catch (error) {\n items.push({\n key: \"messages-db\",\n label: \"Messages DB\",\n status: \"error\",\n detail: classifyDbError(error),\n });\n }\n }\n\n const contactPaths = getContactsDbPaths() ?? [];\n const existingContactPaths = contactPaths.filter((path) => existsSync(path));\n if (existingContactPaths.length === 0) {\n items.push({\n key: \"contacts-db\",\n label: \"Contacts DB\",\n status: \"warn\",\n detail:\n \"No readable Address Book database was found. Message reads still work, but names may stay as raw phone numbers or emails.\",\n });\n } else {\n const failures: string[] = [];\n for (const path of existingContactPaths) {\n try {\n inspectSqlite(path);\n } catch (error) {\n failures.push(`${path}: ${classifyDbError(error)}`);\n }\n }\n items.push({\n key: \"contacts-db\",\n label: \"Contacts DB\",\n status: failures.length === 0 ? \"ok\" : \"warn\",\n detail:\n failures.length === 0\n ? `Readable at ${existingContactPaths.join(\", \")}`\n : `Some contact databases are unreadable. ${failures.join(\" \")}`,\n });\n }\n\n const messagesRunning = await checkMessagesAvailable().catch(() => false);\n items.push({\n key: \"messages-app\",\n label: \"Messages.app\",\n status: messagesRunning ? \"ok\" : \"warn\",\n detail: messagesRunning\n ? \"Messages.app is running.\"\n : \"Messages.app is not running. Reading still works, but sending requires Messages.app to be open.\",\n });\n\n return {\n ok: items.every((item) => item.status !== \"error\"),\n items,\n };\n}\n\nexport function formatAccessReport(report: AccessReport): string {\n const lines = [\"imsg-mcp doctor\", \"\"];\n for (const item of report.items) {\n lines.push(`${iconFor(item.status)} ${item.label}`);\n lines.push(` ${item.detail}`);\n }\n lines.push(\"\");\n lines.push(\n report.ok\n ? \"Environment looks ready for local message reads.\"\n : \"Fix the ERR items above, then rerun `imsg doctor`.\",\n );\n return lines.join(\"\\n\");\n}\n","/**\n * Pure slug generation logic for conversation threads.\n * No side effects; takes data in, returns slug string.\n */\n\nimport { createHash } from \"node:crypto\";\n\n/** Sanitize a name into a slug-safe part: lowercase, spaces to hyphens, strip non-alphanumeric. */\nexport function sanitizeSlugPart(name: string): string {\n return name\n .toLowerCase()\n .replace(/['']/g, \"\")\n .replace(/[\\s_]+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\")\n .replace(/-{2,}/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n/** 4-char hex hash derived from a string (deterministic). */\nexport function shortHash(input: string): string {\n return createHash(\"md5\").update(input).digest(\"hex\").slice(0, 4);\n}\n\n/** Abbreviate service name: \"iMessage\" -> \"imsg\", \"SMS\" -> \"sms\". */\nexport function serviceAbbrev(service: string): string {\n const lower = service.toLowerCase();\n if (lower === \"imessage\") return \"imsg\";\n if (lower === \"sms\") return \"sms\";\n return sanitizeSlugPart(lower) || \"msg\";\n}\n\n/** True if a chat_identifier looks like a group chat (starts with \"chat\"). */\nexport function isGroupChatIdentifier(chatIdentifier: string): boolean {\n return chatIdentifier.startsWith(\"chat\");\n}\n\n/** True if a chat guid indicates a group chat (contains \";+;\"). */\nexport function isGroupGuid(guid: string): boolean {\n return guid.includes(\";+;\");\n}\n\n/**\n * True if `value` matches the `name~service~hash` slug shape generated\n * above. Used by the CLI to decide whether `imsg send <target>` should\n * treat the target as a slug or as a recipient handle.\n *\n * Specifically distinguishes slugs from:\n * - emails — `~` is legal in the local-part (`user~beta@example.com`),\n * but slugs never have `@`\n * - phone numbers — never have `~`\n * - contact names — typed names don't use `~`\n */\nexport function looksLikeThreadSlug(value: string | undefined): boolean {\n if (!value) return false;\n if (value.includes(\"@\")) return false;\n const parts = value.split(\"~\");\n return parts.length === 3 && parts.every((p) => p.length > 0);\n}\n\nexport interface SlugInput {\n chatIdentifier: string;\n guid: string;\n displayName: string | null;\n serviceName: string | null;\n /** Resolved contact name for 1-on-1 chats (from ContactsDB). */\n resolvedContactName: string | null;\n /**\n * Stable identity key the hash is derived from, so every chat leg of one\n * identity yields the SAME slug. For a 1-on-1 with a resolved contact this is\n * `contact:<contactId>` (covers all the contact's numbers + emails across SMS\n * and iMessage); otherwise the normalized handle; for groups, the chat guid.\n * Falls back to `guid` when not provided.\n */\n identityKey?: string;\n}\n\n/**\n * Generate a thread slug from chat metadata.\n *\n * Format: {sanitized-name}~{service}~{short-hash}\n *\n * The hash is derived from `identityKey` (not the per-chat guid) so a contact\n * whose conversation is split across a phone chat and an email chat — and across\n * SMS + iMessage — collapses to ONE stable slug.\n *\n * Examples:\n * 1-on-1 with contact: alice~imsg~a3f2\n * 1-on-1 no contact: 61451082095~sms~b7c1\n * Named group: weekend-crew~imsg~d4e5\n * Unnamed group: group~imsg~f6a7\n */\nexport function generateThreadSlug(input: SlugInput): string {\n const svc = serviceAbbrev(input.serviceName || \"iMessage\");\n const hash = shortHash(input.identityKey ?? input.guid);\n const isGroup = isGroupGuid(input.guid) || isGroupChatIdentifier(input.chatIdentifier);\n\n let namePart: string;\n if (isGroup) {\n if (input.displayName && !input.displayName.startsWith(\"chat\")) {\n namePart = sanitizeSlugPart(input.displayName);\n } else {\n namePart = \"group\";\n }\n } else {\n if (input.resolvedContactName) {\n namePart = sanitizeSlugPart(input.resolvedContactName);\n } else if (input.displayName && input.displayName !== input.chatIdentifier) {\n namePart = sanitizeSlugPart(input.displayName);\n } else {\n namePart = sanitizeSlugPart(input.chatIdentifier.replace(/^\\+/, \"\"));\n }\n }\n\n if (!namePart) namePart = \"unknown\";\n\n return `${namePart}~${svc}~${hash}`;\n}\n","/**\n * Central shutdown / cleanup registry.\n *\n * Any module can register a cleanup function. On process exit (via signal,\n * stdin EOF, or orphan detection), all registered functions run exactly once\n * before the process exits.\n *\n * Prevents orphaned processes by:\n * 1. Trapping SIGINT, SIGTERM, SIGHUP, SIGQUIT\n * 2. Detecting stdin EOF (parent MCP host died)\n * 3. Watching for parent PID change (reparented to launchd = orphaned)\n */\n\nimport { appendLog } from \"./logger.js\";\n\ntype CleanupFn = () => void | Promise<void>;\n\nconst registry = new Set<CleanupFn>();\nlet shuttingDown = false;\nlet watchdogTimer: ReturnType<typeof setInterval> | null = null;\n\n/**\n * Register a cleanup function to run on shutdown.\n * Functions are called in registration order.\n */\nexport function registerCleanup(fn: CleanupFn): void {\n registry.add(fn);\n}\n\n/**\n * Unregister a previously registered cleanup function.\n */\nexport function unregisterCleanup(fn: CleanupFn): void {\n registry.delete(fn);\n}\n\n/**\n * Trigger graceful shutdown. Runs all cleanup functions, then exits.\n * Safe to call multiple times — only runs once.\n */\nexport async function shutdown(exitCode = 0): Promise<never> {\n if (shuttingDown) {\n // Already shutting down — force exit after 3s safety net\n setTimeout(() => process.exit(exitCode), 3000).unref();\n // Block forever (the timeout or a prior shutdown will exit)\n return new Promise<never>(() => {});\n }\n shuttingDown = true;\n\n // Stop watchdog\n if (watchdogTimer) {\n clearInterval(watchdogTimer);\n watchdogTimer = null;\n }\n\n // Run all cleanup functions (best-effort, don't let one failure block others)\n for (const fn of registry) {\n try {\n await fn();\n } catch {\n // Ignore cleanup errors during shutdown\n }\n }\n registry.clear();\n\n process.exit(exitCode);\n}\n\n/** Synchronous cleanup — last resort on process.on('exit') */\nfunction syncCleanup(): void {\n for (const fn of registry) {\n try {\n // Only call sync-safe functions here; async ones will be no-ops\n const result = fn();\n // If it returns a promise, we can't await it here — just ignore\n if (result && typeof (result as Promise<void>).catch === \"function\") {\n (result as Promise<void>).catch(() => {});\n }\n } catch {\n // Ignore\n }\n }\n}\n\nexport interface ShutdownOpts {\n /**\n * Whether to call `shutdown(70)` on `uncaughtException`. Defaults to\n * `true` — correct for MCP / one-shot CLI where an uncaught error means\n * undefined state and a clean restart is the right answer.\n *\n * **Set to `false` for the TUI**, where killing a long-running interactive\n * session over a transient render or async error wastes the user's work\n * and feels like a phantom crash. The TUI keeps running; the error is\n * still recorded via `appendLog(\"error\", \"uncaught_exception\", …)` so the\n * postmortem trail is intact.\n */\n exitOnUncaughtException?: boolean;\n}\n\n/**\n * Install signal handlers and orphan detection.\n * Call once at process startup.\n */\nexport function installShutdownHandlers(opts: ShutdownOpts = {}): void {\n const exitOnUncaught = opts.exitOnUncaughtException ?? true;\n // Signal handlers — log the signal name before cleanup so the post-mortem\n // NDJSON tells us *why* the process ended (e.g. host sent SIGTERM at $time).\n const onSignal = (signal: string) => {\n try {\n appendLog(\"info\", \"signal_received\", { signal });\n } catch {\n // logger may already be torn down; never fail shutdown over logging.\n }\n shutdown(signal === \"SIGINT\" ? 130 : 0);\n };\n\n for (const sig of [\"SIGINT\", \"SIGTERM\", \"SIGHUP\", \"SIGQUIT\"] as const) {\n process.on(sig, () => onSignal(sig));\n }\n\n // Catch-all for stray async errors. Pre-fix, a single unhandled\n // rejection in a background task (heartbeat / cache sweeper /\n // contact-sync) would crash the MCP without any trace in the NDJSON\n // log — the host saw an EPIPE and reconnected with a fresh PID.\n // We log + record but DO NOT exit; the SDK's per-request error\n // handler already isolates request failures.\n process.on(\"unhandledRejection\", (reason) => {\n try {\n appendLog(\"error\", \"unhandled_rejection\", {\n reason: reason instanceof Error ? reason.message : String(reason),\n stack: reason instanceof Error ? reason.stack : undefined,\n });\n } catch {\n // Logger may be torn down mid-shutdown; never re-throw from here.\n }\n });\n process.on(\"uncaughtException\", (err) => {\n try {\n appendLog(\"error\", \"uncaught_exception\", {\n message: err.message,\n stack: err.stack,\n });\n } catch {\n // Same as above.\n }\n // MCP / CLI policy: shutdown on uncaughtException — state is undefined,\n // a restart is the right answer. TUI policy (opt-in): log + keep\n // running so a transient React/Ink error doesn't kill the user's\n // session mid-compose.\n if (exitOnUncaught) shutdown(70);\n });\n\n // Synchronous last-resort cleanup\n process.on(\"exit\", syncCleanup);\n}\n\n/**\n * Enable stdin EOF detection — when the parent process dies, stdin closes.\n * Essential for MCP stdio servers to detect host death.\n */\nexport function enableStdinEofDetection(): void {\n process.stdin.on(\"end\", () => {\n if (!shuttingDown) shutdown(0);\n });\n process.stdin.resume();\n}\n\n/**\n * Enable parent PID watchdog — detects orphaned processes.\n * If the parent PID changes (reparented to launchd/init), trigger shutdown.\n * Timer is unref'd so it doesn't prevent natural exit.\n */\nexport function enableOrphanWatchdog(intervalMs = 5000): void {\n if (watchdogTimer) return;\n const parentPid = process.ppid;\n\n watchdogTimer = setInterval(() => {\n // ppid changes to 1 (launchd/init) when parent dies\n if (process.ppid === 1 || process.ppid !== parentPid) {\n shutdown(0);\n }\n }, intervalMs);\n watchdogTimer.unref();\n}\n\n/**\n * Check if shutdown is in progress.\n */\nexport function isShuttingDown(): boolean {\n return shuttingDown;\n}\n"],"names":["error"],"mappings":"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"}
|