imsg-mcp 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -2
- package/dist/cli.js +47 -5
- package/dist/cli.js.map +1 -1
- package/dist/{imessage-db-Cz8MIIpG.js → imessage-db-C8WKHFQA.js} +102 -15
- package/dist/imessage-db-C8WKHFQA.js.map +1 -0
- package/dist/index.js +848 -73
- package/dist/index.js.map +1 -1
- package/dist/{meta-DzE7CSrZ.js → meta-CGz9n7A2.js} +2 -2
- package/dist/{meta-DzE7CSrZ.js.map → meta-CGz9n7A2.js.map} +1 -1
- package/dist/{setup-BuXuKAHA.js → setup-BDnz3OuH.js} +2 -2
- package/dist/{setup-BuXuKAHA.js.map → setup-BDnz3OuH.js.map} +1 -1
- package/dist/{shutdown-pPAPSnk3.js → shutdown-CqGQKFmr.js} +73 -47
- package/dist/shutdown-CqGQKFmr.js.map +1 -0
- package/dist/tui.js +216 -60
- package/dist/tui.js.map +1 -1
- package/dist/{watchdog-Cbgm050b.js → watchdog-CizaS0Jv.js} +80 -4
- package/dist/watchdog-CizaS0Jv.js.map +1 -0
- package/native/imsg-native.darwin-arm64.node +0 -0
- package/package.json +1 -1
- package/skills/humans/SKILL.md +136 -0
- package/skills/imsg-mcp/SKILL.md +8 -0
- package/dist/imessage-db-Cz8MIIpG.js.map +0 -1
- package/dist/shutdown-pPAPSnk3.js.map +0 -1
- package/dist/watchdog-Cbgm050b.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { I as isGroupChatIdentifier, J as isShuttingDown, y as info, K as warn, L as error, u as registerCleanup, a as shutdown } from "./shutdown-CqGQKFmr.js";
|
|
1
2
|
import { writeFileSync } from "node:fs";
|
|
2
3
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
3
|
-
import {
|
|
4
|
+
import { getHeapSpaceStatistics, getHeapStatistics } from "node:v8";
|
|
4
5
|
const MS_PER_DAY = 864e5;
|
|
5
6
|
function localDateKey(d) {
|
|
6
7
|
const y = d.getFullYear();
|
|
@@ -238,13 +239,68 @@ function computeWrapped(messages) {
|
|
|
238
239
|
longestStreakContact: top?.contact ?? null
|
|
239
240
|
};
|
|
240
241
|
}
|
|
242
|
+
function computeRelationshipLeaderboard(messages) {
|
|
243
|
+
const byChat = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const m of messages) {
|
|
245
|
+
if (m.isReaction) continue;
|
|
246
|
+
if (!m.chatId || isGroupChatIdentifier(m.chatId)) continue;
|
|
247
|
+
let acc = byChat.get(m.chatId);
|
|
248
|
+
if (!acc) {
|
|
249
|
+
acc = { contact: null, handle: m.chatId, sent: 0, received: 0, lastMs: 0 };
|
|
250
|
+
byChat.set(m.chatId, acc);
|
|
251
|
+
}
|
|
252
|
+
if (m.isFromMe) {
|
|
253
|
+
acc.sent++;
|
|
254
|
+
} else {
|
|
255
|
+
acc.received++;
|
|
256
|
+
if (!acc.contact && m.displayName) acc.contact = m.displayName;
|
|
257
|
+
if (m.handle && m.handle !== "unknown") acc.handle = m.handle;
|
|
258
|
+
}
|
|
259
|
+
if (m.date.getTime() > acc.lastMs) acc.lastMs = m.date.getTime();
|
|
260
|
+
}
|
|
261
|
+
const byContact = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const acc of byChat.values()) {
|
|
263
|
+
const key = acc.contact ?? acc.handle;
|
|
264
|
+
if (!key || key === "unknown") continue;
|
|
265
|
+
const merged = byContact.get(key);
|
|
266
|
+
if (merged) {
|
|
267
|
+
merged.sent += acc.sent;
|
|
268
|
+
merged.received += acc.received;
|
|
269
|
+
merged.lastMs = Math.max(merged.lastMs, acc.lastMs);
|
|
270
|
+
} else {
|
|
271
|
+
byContact.set(key, { ...acc });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const leaderboard = [];
|
|
276
|
+
for (const [contact, acc] of byContact) {
|
|
277
|
+
const total = acc.sent + acc.received;
|
|
278
|
+
if (total < 3) continue;
|
|
279
|
+
const reciprocity = Math.min(acc.sent, acc.received) / Math.max(Math.max(acc.sent, acc.received), 1);
|
|
280
|
+
const daysSinceLast = Math.max(0, (now - acc.lastMs) / 864e5);
|
|
281
|
+
const score = Math.log2(1 + total) * (0.4 + 0.6 * reciprocity) * Math.exp(-daysSinceLast / 45);
|
|
282
|
+
leaderboard.push({
|
|
283
|
+
contact,
|
|
284
|
+
handle: acc.handle,
|
|
285
|
+
total,
|
|
286
|
+
sent: acc.sent,
|
|
287
|
+
received: acc.received,
|
|
288
|
+
reciprocity: Math.round(reciprocity * 100) / 100,
|
|
289
|
+
daysSinceLast: Math.round(daysSinceLast * 10) / 10,
|
|
290
|
+
score: Math.round(score * 1e3) / 1e3
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
leaderboard.sort((a, b) => b.score - a.score);
|
|
294
|
+
return { leaderboard: leaderboard.slice(0, 50) };
|
|
295
|
+
}
|
|
241
296
|
const IMPLEMENTED_TYPES = [
|
|
242
297
|
"messaging_streaks",
|
|
243
298
|
"double_texts",
|
|
244
299
|
"response_time_stats",
|
|
245
300
|
"daily_heatmap",
|
|
246
301
|
"tapback_summary",
|
|
247
|
-
"year_in_review_wrapped"
|
|
302
|
+
"year_in_review_wrapped",
|
|
303
|
+
"relationship_leaderboard"
|
|
248
304
|
];
|
|
249
305
|
function dispatchAnalytic(type, messages) {
|
|
250
306
|
switch (type) {
|
|
@@ -260,6 +316,8 @@ function dispatchAnalytic(type, messages) {
|
|
|
260
316
|
return { type, data: computeTapbacks(messages) };
|
|
261
317
|
case "year_in_review_wrapped":
|
|
262
318
|
return { type, data: computeWrapped(messages) };
|
|
319
|
+
case "relationship_leaderboard":
|
|
320
|
+
return { type, data: computeRelationshipLeaderboard(messages) };
|
|
263
321
|
}
|
|
264
322
|
}
|
|
265
323
|
const EMOJI_REGEX = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}]/gu;
|
|
@@ -463,6 +521,7 @@ function resolveRecipient(input, ctx) {
|
|
|
463
521
|
};
|
|
464
522
|
}
|
|
465
523
|
if (candidates.length === 1) return candidates[0];
|
|
524
|
+
if (matches.length === 1) return candidates[0];
|
|
466
525
|
return { kind: "ambiguous", query: trimmed, candidates };
|
|
467
526
|
}
|
|
468
527
|
function minMessageId(messages) {
|
|
@@ -610,6 +669,22 @@ function installWatchdog(opts = {}) {
|
|
|
610
669
|
state.heapHistory.shift();
|
|
611
670
|
}
|
|
612
671
|
if (rssMb >= MAX_RSS_MB) {
|
|
672
|
+
try {
|
|
673
|
+
error("rss_kill_heap_forensics", {
|
|
674
|
+
heap_stats: getHeapStatistics(),
|
|
675
|
+
heap_spaces: getHeapSpaceStatistics().map((sp) => ({
|
|
676
|
+
name: sp.space_name,
|
|
677
|
+
used_mb: round1(sp.space_used_size / 1024 / 1024)
|
|
678
|
+
})),
|
|
679
|
+
memory_usage: {
|
|
680
|
+
rss_mb: rssMb,
|
|
681
|
+
heap_used_mb: heapMb,
|
|
682
|
+
external_mb: round1(mu.external / 1024 / 1024),
|
|
683
|
+
array_buffers_mb: round1(mu.arrayBuffers / 1024 / 1024)
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
} catch {
|
|
687
|
+
}
|
|
613
688
|
triggerKill("rss_exceeded", { rss_mb: rssMb, threshold_mb: MAX_RSS_MB });
|
|
614
689
|
return;
|
|
615
690
|
}
|
|
@@ -677,12 +752,13 @@ export {
|
|
|
677
752
|
IMPLEMENTED_TYPES as I,
|
|
678
753
|
rankFuzzy as a,
|
|
679
754
|
dispatchAnalytic as b,
|
|
680
|
-
|
|
755
|
+
computeRelationshipLeaderboard as c,
|
|
681
756
|
defaultCountryFromEnv as d,
|
|
757
|
+
readWatchdogState as e,
|
|
682
758
|
installWatchdog as i,
|
|
683
759
|
minMessageId as m,
|
|
684
760
|
noteActivity as n,
|
|
685
761
|
onMemorySample as o,
|
|
686
762
|
resolveRecipient as r
|
|
687
763
|
};
|
|
688
|
-
//# sourceMappingURL=watchdog-
|
|
764
|
+
//# sourceMappingURL=watchdog-CizaS0Jv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdog-CizaS0Jv.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 { isGroupChatIdentifier } from \"./thread-slug.js\";\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// ── Relationship leaderboard ─────────────────────────────────────────────\n\nexport interface RelationshipScore {\n /** Contact display name when resolved, else the raw handle. */\n contact: string;\n /** A representative handle for the contact (feeds init_human / get_contact). */\n handle: string;\n total: number;\n sent: number;\n received: number;\n /** min(sent,received)/max(sent,received) — 1.0 = perfectly balanced. */\n reciprocity: number;\n daysSinceLast: number;\n /** Weighted importance score — see computeRelationshipLeaderboard. */\n score: number;\n}\n\n/**\n * Rank 1:1 relationships by fuzzy importance within the analytic window.\n *\n * Score = log2(1 + total) — volume with diminishing returns\n * × (0.4 + 0.6 × reciprocity) — real conversations beat broadcasts\n * × exp(-daysSinceLast / 45) — recency decay (half-ish life ~1 month)\n *\n * Group chats are excluded (a person's group activity says little about the\n * 1:1 relationship, and humans files are per-person). Contacts are keyed by\n * resolved display name so phone+email legs of one person merge; unresolved\n * handles key by the handle itself.\n */\nexport function computeRelationshipLeaderboard(messages: Message[]): {\n leaderboard: RelationshipScore[];\n} {\n interface Acc {\n contact: string | null;\n handle: string;\n sent: number;\n received: number;\n lastMs: number;\n }\n // Group by CONVERSATION (chatId), not by sender — from-me rows carry the\n // user's own handle (\"me\"), so sender-keyed grouping put every sent message\n // under one giant \"me\" bucket and zeroed reciprocity for every contact.\n const byChat = new Map<string, Acc>();\n for (const m of messages) {\n if (m.isReaction) continue;\n if (!m.chatId || isGroupChatIdentifier(m.chatId)) continue;\n let acc = byChat.get(m.chatId);\n if (!acc) {\n acc = { contact: null, handle: m.chatId, sent: 0, received: 0, lastMs: 0 };\n byChat.set(m.chatId, acc);\n }\n if (m.isFromMe) {\n acc.sent++;\n } else {\n acc.received++;\n // The other party names the conversation.\n if (!acc.contact && m.displayName) acc.contact = m.displayName;\n if (m.handle && m.handle !== \"unknown\") acc.handle = m.handle;\n }\n if (m.date.getTime() > acc.lastMs) acc.lastMs = m.date.getTime();\n }\n\n // Merge phone+email legs of the same person: same resolved contact name →\n // one entry.\n const byContact = new Map<string, Acc>();\n for (const acc of byChat.values()) {\n const key = acc.contact ?? acc.handle;\n if (!key || key === \"unknown\") continue;\n const merged = byContact.get(key);\n if (merged) {\n merged.sent += acc.sent;\n merged.received += acc.received;\n merged.lastMs = Math.max(merged.lastMs, acc.lastMs);\n } else {\n byContact.set(key, { ...acc });\n }\n }\n\n const now = Date.now();\n const leaderboard: RelationshipScore[] = [];\n for (const [contact, acc] of byContact) {\n const total = acc.sent + acc.received;\n // One-sided \"conversations\" (promos, OTP senders) score near zero via\n // the reciprocity term; skip trivial volumes outright.\n if (total < 3) continue;\n const reciprocity =\n Math.min(acc.sent, acc.received) / Math.max(Math.max(acc.sent, acc.received), 1);\n const daysSinceLast = Math.max(0, (now - acc.lastMs) / 86_400_000);\n const score = Math.log2(1 + total) * (0.4 + 0.6 * reciprocity) * Math.exp(-daysSinceLast / 45);\n leaderboard.push({\n contact,\n handle: acc.handle,\n total,\n sent: acc.sent,\n received: acc.received,\n reciprocity: Math.round(reciprocity * 100) / 100,\n daysSinceLast: Math.round(daysSinceLast * 10) / 10,\n score: Math.round(score * 1000) / 1000,\n });\n }\n leaderboard.sort((a, b) => b.score - a.score);\n return { leaderboard: leaderboard.slice(0, 50) };\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 | \"relationship_leaderboard\";\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 \"relationship_leaderboard\",\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 case \"relationship_leaderboard\":\n return { type, data: computeRelationshipLeaderboard(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 // All handles belonging to ONE contact card are not ambiguous — that's just\n // one person with several ways to reach them. Prefer their first phone\n // (dial-ready) over emails. Pre-fix, \"send to dad\" listed Dad's own three\n // handles as competing \"matches\".\n if (matches.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 /** chat.db `error` code when a from-me message failed to send (e.g. 22 = wrong-service iMessage attempt). Undefined = sent fine. */\n sendError?: number;\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 /** attachment table ROWID — feed to get_attachment to fetch/view the file. */\n rowId?: number;\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 { getHeapSpaceStatistics, getHeapStatistics } from \"node:v8\";\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 // Forensics before dying: a real incident (2026-07-12) went 160MB →\n // 1071MB inside one 60s sample window with no completed perf span to\n // name the allocator. V8 heap-space stats are cheap and identify WHERE\n // the memory sits (old space vs external vs malloc'd), which separates\n // \"JS object leak\" from \"native/Buffer growth\" for the next occurrence.\n try {\n error(\"rss_kill_heap_forensics\", {\n heap_stats: getHeapStatistics(),\n heap_spaces: getHeapSpaceStatistics().map((sp) => ({\n name: sp.space_name,\n used_mb: round1(sp.space_used_size / 1024 / 1024),\n })),\n memory_usage: {\n rss_mb: rssMb,\n heap_used_mb: heapMb,\n external_mb: round1(mu.external / 1024 / 1024),\n array_buffers_mb: round1(mu.arrayBuffers / 1024 / 1024),\n },\n });\n } catch {\n // Forensics must never block the kill.\n }\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":";;;;AAkBA,MAAM,aAAa;AAGnB,SAAS,aAAa,GAAiB;AACrC,QAAM,IAAI,EAAE,YAAA;AACZ,QAAM,IAAI,OAAO,EAAE,SAAA,IAAa,CAAC,EAAE,SAAS,GAAG,GAAG;AAClD,QAAM,MAAM,OAAO,EAAE,QAAA,CAAS,EAAE,SAAS,GAAG,GAAG;AAC/C,SAAO,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG;AACzB;AAGA,SAAS,WAAW,GAAoB;AACtC,SAAO,EAAE;AACX;AAGA,SAAS,OAAO,GAAY,GAAoB;AAC9C,SAAO,EAAE,KAAK,QAAA,IAAY,EAAE,KAAK,QAAA;AACnC;AAYO,SAAS,eAAe,UAAqC;AAClE,QAAM,gCAAgB,IAAA;AACtB,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,WAAY;AAClB,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,UAAU,IAAI,CAAC,aAAa,IAAI,GAAG,oBAAI,KAAK;AACjD,cAAU,IAAI,CAAC,EAAG,IAAI,aAAa,EAAE,IAAI,CAAC;AAAA,EAC5C;AAEc,eAAa,oBAAI,KAAA,CAAM;AACrC,QAAM,UAA0B,CAAA;AAEhC,aAAW,CAAC,SAAS,MAAM,KAAK,WAAW;AACzC,UAAM,OAAO,MAAM,KAAK,MAAM,EAAE,KAAA;AAChC,QAAI,UAAU;AACd,QAAI,eAA8B;AAClC,QAAI,aAA4B;AAChC,QAAI,SAAS;AACb,QAAI,WAA0B;AAE9B,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UACE,MAAM,KACN,IAAI,KAAK,KAAK,CAAC,CAAE,EAAE,QAAA,IAAY,IAAI,KAAK,KAAK,IAAI,CAAC,CAAE,EAAE,QAAA,MAAc,YACpE;AACA,iBAAS;AACT,mBAAW,KAAK,CAAC;AAAA,MACnB,OAAO;AACL;AAAA,MACF;AACA,UAAI,SAAS,SAAS;AACpB,kBAAU;AACV,uBAAe;AACf,qBAAa,KAAK,CAAC;AAAA,MACrB;AAAA,IACF;AAGA,QAAI,SAAS;AACb,UAAM,6BAAa,KAAA;AACnB,WAAO,OAAO,IAAI,aAAa,MAAM,CAAC,GAAG;AACvC;AACA,aAAO,QAAQ,OAAO,QAAA,IAAY,CAAC;AAAA,IACrC;AAEA,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,MACpB,kBAAkB;AAAA,MAClB,mBAAmB;AAAA,IAAA,CACpB;AAAA,EAEH;AAEA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,oBAAoB,EAAE,iBAAiB;AAChE,SAAO;AACT;AAUO,SAAS,mBAAmB,UAAyC;AAC1E,QAAM,gCAAgB,IAAA;AACtB,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,WAAY;AAClB,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,UAAU,IAAI,CAAC,EAAG,WAAU,IAAI,GAAG,EAAE;AAC1C,cAAU,IAAI,CAAC,EAAG,KAAK,CAAC;AAAA,EAC1B;AAEA,QAAM,UAA8B,CAAA;AACpC,aAAW,CAAC,SAAS,IAAI,KAAK,WAAW;AACvC,SAAK,KAAK,MAAM;AAChB,QAAI,UAAU;AACd,QAAI,YAAY;AAChB,QAAI,cAAc;AAClB,QAAI,gBAAgB;AACpB,eAAW,KAAK,MAAM;AACpB,UAAI,EAAE,UAAU;AACd;AACA,oBAAY;AACZ,YAAI,WAAW,EAAG;AAAA,MACpB,OAAO;AACL;AACA,kBAAU;AACV,YAAI,aAAa,EAAG;AAAA,MACtB;AAAA,IACF;AACA,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,mBAAmB;AAAA,MACnB,qBAAqB;AAAA,IAAA,CACtB;AAAA,EACH;AACA,UAAQ;AAAA,IACN,CAAC,GAAG,MACF,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE;AAAA,EAAA;AAE3E,SAAO;AACT;AAYA,SAAS,WAAW,QAAkB,GAAmB;AACvD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,MAAM,KAAK,IAAI,OAAO,SAAS,GAAG,KAAK,MAAO,IAAI,MAAO,OAAO,MAAM,CAAC;AAC7E,SAAO,OAAO,GAAG,KAAK;AACxB;AAEO,SAAS,qBAAqB,UAA0C;AAC7E,QAAM,gCAAgB,IAAA;AACtB,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,WAAY;AAClB,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,UAAU,IAAI,CAAC,EAAG,WAAU,IAAI,GAAG,EAAE;AAC1C,cAAU,IAAI,CAAC,EAAG,KAAK,CAAC;AAAA,EAC1B;AACA,QAAM,UAA+B,CAAA;AACrC,aAAW,CAAC,SAAS,IAAI,KAAK,WAAW;AACvC,SAAK,KAAK,MAAM;AAChB,UAAM,SAAmB,CAAA;AACzB,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,OAAO,KAAK,IAAI,CAAC;AACvB,YAAM,OAAO,KAAK,CAAC;AAEnB,UAAI,CAAC,KAAK,YAAY,KAAK,UAAU;AACnC,eAAO,KAAK,KAAK,KAAK,QAAA,IAAY,KAAK,KAAK,SAAS;AAAA,MACvD;AAAA,IACF;AACA,QAAI,OAAO,WAAW,EAAG;AACzB,WAAO,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC3B,UAAM,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,OAAO;AACxD,YAAQ,KAAK;AAAA,MACX;AAAA,MACA,OAAO,OAAO;AAAA,MACd,UAAU,WAAW,QAAQ,EAAE;AAAA,MAC/B,OAAO,WAAW,QAAQ,EAAE;AAAA,MAC5B,QAAQ,KAAK,MAAM,IAAI;AAAA,IAAA,CACxB;AAAA,EACH;AACA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAC9C,SAAO;AACT;AAUO,SAAS,eAAe,UAAoC;AACjE,QAAM,OAAmB,MAAM,KAAK,EAAE,QAAQ,KAAK,MAAM,MAAM,KAAK,EAAE,QAAQ,GAAA,GAAM,MAAM,CAAC,CAAC;AAC5F,MAAI,QAAQ;AACZ,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,WAAY;AAClB,UAAM,MAAM,EAAE,KAAK,OAAA;AACnB,UAAM,KAAK,EAAE,KAAK,SAAA;AAClB,UAAM,SAAS,KAAK,GAAG;AACvB,QAAI,UAAU,MAAM,KAAK,KAAK,IAAI;AAChC,aAAO,EAAE,KAAK,OAAO,EAAE,KAAK,KAAK;AACjC;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,MAAM,MAAA;AACjB;AA0BO,SAAS,gBAAgB,UAAsC;AACpE,QAAM,gCAAgB,IAAA;AACtB,aAAW,KAAK,UAAU;AACxB,QAAI,CAAC,EAAE,cAAc,CAAC,EAAE,SAAU;AAClC,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,UAAU,IAAI,CAAC,GAAG;AACrB,gBAAU,IAAI,GAAG;AAAA,QACf,SAAS;AAAA,QACT,OAAO;AAAA,QACP,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,MAAM;AAAA,QACN,SAAS;AAAA,QACT,UAAU;AAAA,QACV,OAAO;AAAA,QACP,OAAO;AAAA,MAAA,CACR;AAAA,IACH;AACA,UAAM,IAAI,UAAU,IAAI,CAAC;AAIzB,UAAM,KAAK,EAAE,SAAS,QAAQ,IAAI,YAAA;AAClC,QAAI,EAAE,SAAS,OAAO,KAAK,MAAM,OAAQ,GAAE;AAAA,aACjC,EAAE,SAAS,OAAO,KAAK,EAAE,SAAS,IAAI,KAAM,MAAM,OAAQ,GAAE;AAAA,aAC5D,EAAE,SAAS,OAAO,KAAK,EAAE,SAAS,MAAM,KAAM,MAAM,UAAW,GAAE;AAAA,aAClE,EAAE,SAAS,MAAM,KAAK,MAAM,QAAS,GAAE;AAAA,aACvC,EAAE,SAAS,SAAS,KAAK,MAAM,WAAY,GAAE;AAAA,aAC7C,EAAE,SAAS,UAAU,EAAG,GAAE;AAAA,QAC9B,GAAE;AACP,MAAE;AAAA,EAEJ;AACA,QAAM,MAAM,MAAM,KAAK,UAAU,QAAQ;AACzC,MAAI,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACpC,SAAO;AACT;AAgBO,SAAS,eAAe,UAAoC;AACjE,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,MACL,aAAa;AAAA,MACb,WAAW;AAAA,MACX,WAAW;AAAA,MACX,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,aAAa,CAAA;AAAA,MACb,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,sBAAsB;AAAA,IAAA;AAAA,EAE1B;AACA,MAAI,OAAO;AACX,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,QAAM,iCAAiB,IAAA;AACvB,QAAM,6BAAa,IAAA;AACnB,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,YAAY;AAChB;AACA;AAAA,IACF;AACA,QAAI,EAAE,SAAU;AAAA,QACX;AACL,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,WAAW,IAAI,CAAC,EAAG,YAAW,IAAI,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG;AAC5E,UAAM,KAAK,WAAW,IAAI,CAAC;AAC3B,QAAI,EAAE,SAAU,IAAG;AAAA,QACd,IAAG;AACR,OAAG;AACH,UAAM,KAAK,aAAa,EAAE,IAAI;AAC9B,WAAO,IAAI,KAAK,OAAO,IAAI,EAAE,KAAK,KAAK,CAAC;AAAA,EAC1C;AAEA,QAAM,cAAc,MAAM,KAAK,WAAW,QAAA,CAAS,EAChD,IAAI,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,GAAG,EAAA,EAAI,EACzC,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAChC,MAAM,GAAG,CAAC;AAEb,MAAI,UAAkD;AACtD,aAAW,CAAC,GAAG,KAAK,KAAK,QAAQ;AAC/B,QAAI,CAAC,WAAW,QAAQ,QAAQ,MAAO,WAAU,EAAE,MAAM,GAAG,MAAA;AAAA,EAC9D;AAGA,QAAM,UAAU,eAAe,QAAQ;AACvC,QAAM,MAAM,QAAQ,CAAC;AAErB,SAAO;AAAA,IACL,aAAa,aAAa,SAAS,CAAC,EAAG,IAAI;AAAA,IAC3C,WAAW,aAAa,SAAS,SAAS,SAAS,CAAC,EAAG,IAAI;AAAA,IAC3D,WAAW;AAAA,IACX,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA,mBAAmB,KAAK,qBAAqB;AAAA,IAC7C,sBAAsB,KAAK,WAAW;AAAA,EAAA;AAE1C;AA+BO,SAAS,+BAA+B,UAE7C;AAWA,QAAM,6BAAa,IAAA;AACnB,aAAW,KAAK,UAAU;AACxB,QAAI,EAAE,WAAY;AAClB,QAAI,CAAC,EAAE,UAAU,sBAAsB,EAAE,MAAM,EAAG;AAClD,QAAI,MAAM,OAAO,IAAI,EAAE,MAAM;AAC7B,QAAI,CAAC,KAAK;AACR,YAAM,EAAE,SAAS,MAAM,QAAQ,EAAE,QAAQ,MAAM,GAAG,UAAU,GAAG,QAAQ,EAAA;AACvE,aAAO,IAAI,EAAE,QAAQ,GAAG;AAAA,IAC1B;AACA,QAAI,EAAE,UAAU;AACd,UAAI;AAAA,IACN,OAAO;AACL,UAAI;AAEJ,UAAI,CAAC,IAAI,WAAW,EAAE,YAAa,KAAI,UAAU,EAAE;AACnD,UAAI,EAAE,UAAU,EAAE,WAAW,UAAW,KAAI,SAAS,EAAE;AAAA,IACzD;AACA,QAAI,EAAE,KAAK,QAAA,IAAY,IAAI,OAAQ,KAAI,SAAS,EAAE,KAAK,QAAA;AAAA,EACzD;AAIA,QAAM,gCAAgB,IAAA;AACtB,aAAW,OAAO,OAAO,UAAU;AACjC,UAAM,MAAM,IAAI,WAAW,IAAI;AAC/B,QAAI,CAAC,OAAO,QAAQ,UAAW;AAC/B,UAAM,SAAS,UAAU,IAAI,GAAG;AAChC,QAAI,QAAQ;AACV,aAAO,QAAQ,IAAI;AACnB,aAAO,YAAY,IAAI;AACvB,aAAO,SAAS,KAAK,IAAI,OAAO,QAAQ,IAAI,MAAM;AAAA,IACpD,OAAO;AACL,gBAAU,IAAI,KAAK,EAAE,GAAG,KAAK;AAAA,IAC/B;AAAA,EACF;AAEA,QAAM,MAAM,KAAK,IAAA;AACjB,QAAM,cAAmC,CAAA;AACzC,aAAW,CAAC,SAAS,GAAG,KAAK,WAAW;AACtC,UAAM,QAAQ,IAAI,OAAO,IAAI;AAG7B,QAAI,QAAQ,EAAG;AACf,UAAM,cACJ,KAAK,IAAI,IAAI,MAAM,IAAI,QAAQ,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,MAAM,IAAI,QAAQ,GAAG,CAAC;AACjF,UAAM,gBAAgB,KAAK,IAAI,IAAI,MAAM,IAAI,UAAU,KAAU;AACjE,UAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,MAAM,eAAe,KAAK,IAAI,CAAC,gBAAgB,EAAE;AAC7F,gBAAY,KAAK;AAAA,MACf;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,MAAM,IAAI;AAAA,MACV,UAAU,IAAI;AAAA,MACd,aAAa,KAAK,MAAM,cAAc,GAAG,IAAI;AAAA,MAC7C,eAAe,KAAK,MAAM,gBAAgB,EAAE,IAAI;AAAA,MAChD,OAAO,KAAK,MAAM,QAAQ,GAAI,IAAI;AAAA,IAAA,CACnC;AAAA,EACH;AACA,cAAY,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC5C,SAAO,EAAE,aAAa,YAAY,MAAM,GAAG,EAAE,EAAA;AAC/C;AAqCO,MAAM,oBAAoC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,iBACd,MACA,UAIA;AACA,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,eAAe,QAAQ,EAAA;AAAA,IAC9C,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,mBAAmB,QAAQ,EAAA;AAAA,IAClD,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,qBAAqB,QAAQ,EAAA;AAAA,IACpD,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,eAAe,QAAQ,EAAA;AAAA,IAC9C,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,gBAAgB,QAAQ,EAAA;AAAA,IAC/C,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,eAAe,QAAQ,EAAA;AAAA,IAC9C,KAAK;AACH,aAAO,EAAE,MAAM,MAAM,+BAA+B,QAAQ,EAAA;AAAA,EAAE;AAEpE;AC7gBA,MAAM,cAAc;AACpB,MAAM,mBAAmB;AAGlB,SAAS,UAAU,GAAmB;AAC3C,SAAO,EAAE,cAAc,QAAQ,aAAa,GAAG,EAAE,QAAQ,kBAAkB,GAAG,EAAE,KAAA;AAClF;AAGA,SAAS,SAAS,GAAwB;AACxC,MAAI,CAAC,EAAG,QAAO,oBAAI,IAAA;AACnB,SAAO,IAAI,IAAI,EAAE,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;AAC7C;AAQA,SAAS,YAAY,GAAW,GAAW,SAAS,KAAa;AAC/D,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,OAAO,KAAK,IAAI,EAAE,QAAQ,MAAM;AACtC,QAAM,OAAO,KAAK,IAAI,EAAE,QAAQ,MAAM;AACtC,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AAEvB,MAAI,OAAO,IAAI,MAAc,OAAO,CAAC;AACrC,MAAI,OAAO,IAAI,MAAc,OAAO,CAAC;AACrC,WAAS,IAAI,GAAG,KAAK,MAAM,IAAK,MAAK,CAAC,IAAI;AAE1C,WAAS,IAAI,GAAG,KAAK,MAAM,KAAK;AAC9B,SAAK,CAAC,IAAI;AACV,aAAS,IAAI,GAAG,KAAK,MAAM,KAAK;AAC9B,YAAM,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI;AACzC,YAAM,OAAO,KAAK,CAAC,KAAK,KAAK;AAC7B,YAAM,OAAO,KAAK,IAAI,CAAC,KAAK,KAAK;AACjC,YAAM,OAAO,KAAK,IAAI,CAAC,KAAK,KAAK;AACjC,WAAK,CAAC,IAAI,KAAK,IAAI,KAAK,KAAK,GAAG;AAAA,IAClC;AACA,KAAC,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI;AAAA,EAC5B;AACA,SAAO,KAAK,IAAI,KAAK,KAAK,IAAI,MAAM,IAAI;AAC1C;AAGA,SAAS,iBAAiB,GAAW,GAAmB;AACtD,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,OAAO,KAAK,IAAI,EAAE,QAAQ,GAAG;AACnC,QAAM,OAAO,KAAK,IAAI,EAAE,QAAQ,GAAG;AACnC,QAAM,SAAS,KAAK,IAAI,MAAM,IAAI;AAClC,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,IAAI,YAAY,GAAG,CAAC,IAAI;AACjC;AAQO,SAAS,WAAW,OAAe,WAA2B;AAMnE,MAAI,SAAS,aAAa,UAAU,YAAA,EAAc,SAAS,MAAM,YAAA,CAAa,GAAG;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,IAAI,UAAU,KAAK;AACzB,QAAM,IAAI,UAAU,SAAS;AAC7B,MAAI,CAAC,KAAK,CAAC,EAAG,QAAO;AAGrB,MAAI,EAAE,SAAS,CAAC,EAAG,QAAO;AAC1B,MAAI,EAAE,SAAS,CAAC,EAAG,QAAO;AAG1B,QAAM,UAAU,SAAS,CAAC;AAC1B,QAAM,UAAU,SAAS,CAAC;AAC1B,MAAI,eAAe;AACnB,aAAW,OAAO,SAAS;AACzB,QAAI,QAAQ,IAAI,GAAG,EAAG;AAAA,EACxB;AACA,QAAM,OAAQ,IAAI,gBAAiB,QAAQ,OAAO,QAAQ,QAAQ;AAKlE,MAAI,eAAe;AACnB,MAAI,QAAQ,SAAS,GAAG;AACtB,eAAW,OAAO,SAAS;AACzB,YAAM,IAAI,iBAAiB,GAAG,GAAG;AACjC,UAAI,IAAI,aAAc,gBAAe;AAAA,IACvC;AAAA,EACF;AACA,QAAM,WAAW,KAAK,IAAI,iBAAiB,GAAG,CAAC,GAAG,YAAY;AAK9D,QAAM,QAAQ,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,WAAW,MAAM,OAAO,MAAM;AAEnF,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAGO,SAAS,UACd,OACA,YACA,SACA,WAAW,KACwB;AACnC,QAAM,MAAyC,CAAA;AAC/C,aAAW,KAAK,YAAY;AAC1B,UAAM,QAAQ,WAAW,OAAO,QAAQ,CAAC,CAAC;AAC1C,QAAI,SAAS,SAAU,KAAI,KAAK,EAAE,MAAM,GAAG,OAAO;AAAA,EACpD;AACA,MAAI,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACpC,SAAO;AACT;ACnFO,SAAS,wBAAqC;AACnD,QAAM,OAAO,QAAQ,IAAI,wBAAwB,IAAI,YAAA,EAAc,KAAA;AACnE,MAAI,QAAQ,KAAM,QAAO;AACzB,SAAO;AACT;AAGA,MAAM,gBAAgB;AAGtB,MAAM,uBAAuB;AAM7B,MAAM,aAAqC;AAAA,EACzC,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL;AASA,SAAS,uBAAuB,GAAmB;AACjD,MAAI,MAAM;AACV,aAAW,MAAM,GAAG;AAClB,UAAM,QAAQ,GAAG,YAAA;AACjB,WAAO,WAAW,KAAK,KAAK;AAAA,EAC9B;AACA,SAAO;AACT;AAOA,SAAS,cAAc,OAAwB;AAC7C,MAAI,CAAC,qBAAqB,KAAK,KAAK,EAAG,QAAO;AAC9C,QAAM,YAAY,WAAW,KAAK,KAAK;AACvC,QAAM,WAAW,KAAK,KAAK,KAAK;AAChC,SAAO,aAAa;AACtB;AAgBO,SAAS,qBACd,OACA,iBAA8B,MACf;AACf,MAAI,UAAU,MAAM,KAAA;AACpB,MAAI,CAAC,QAAS,QAAO;AAErB,MAAI,cAAc,OAAO,GAAG;AAC1B,cAAU,uBAAuB,OAAO;AAAA,EAC1C;AACA,MAAI,CAAC,cAAc,KAAK,OAAO,EAAG,QAAO;AAEzC,QAAM,SAAS,QAAQ,QAAQ,OAAO,EAAE;AACxC,MAAI,OAAO,WAAW,EAAG,QAAO;AAGhC,MAAI,QAAQ,WAAW,GAAG,GAAG;AAC3B,QAAI,OAAO,SAAS,KAAK,OAAO,SAAS,GAAI,QAAO;AACpD,WAAO,IAAI,MAAM;AAAA,EACnB;AAGA,MAAI,mBAAmB,MAAM;AAE3B,QAAI,OAAO,WAAW,MAAM,OAAO,WAAW,GAAG,GAAG;AAClD,aAAO,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA,IAC9B;AAEA,QAAI,OAAO,WAAW,KAAK,OAAO,WAAW,GAAG,GAAG;AACjD,aAAO,MAAM,MAAM;AAAA,IACrB;AAEA,QAAI,OAAO,WAAW,MAAM,OAAO,WAAW,IAAI,GAAG;AACnD,aAAO,IAAI,MAAM;AAAA,IACnB;AAAA,EACF;AAEA,MAAI,mBAAmB,MAAM;AAE3B,QAAI,OAAO,WAAW,GAAI,QAAO,KAAK,MAAM;AAE5C,QAAI,OAAO,WAAW,MAAM,OAAO,WAAW,GAAG,EAAG,QAAO,IAAI,MAAM;AAAA,EACvE;AAIA,MAAI,OAAO,UAAU,KAAK,OAAO,UAAU,GAAI,QAAO,IAAI,MAAM;AAEhE,SAAO;AACT;AAGA,MAAM,WAAW;AAEV,SAAS,cAAc,OAAwB;AACpD,SAAO,SAAS,KAAK,MAAM,KAAA,CAAM;AACnC;AAMO,SAAS,iBACd,OACA,KACqB;AACrB,QAAM,UAAU,MAAM,KAAA;AACtB,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,MAAM,SAAS,SAAS,yBAAA;AAAA,EACnC;AAGA,MAAI,cAAc,OAAO,GAAG;AAC1B,UAAM,SAAS,QAAQ,YAAA;AACvB,WAAO,EAAE,MAAM,SAAS,QAAQ,aAAa,OAAA;AAAA,EAC/C;AAGA,MAAI,cAAc,KAAK,OAAO,KAAK,cAAc,OAAO,GAAG;AACzD,UAAM,OAAO,qBAAqB,SAAS,IAAI,kBAAkB,IAAI;AACrE,QAAI,MAAM;AACR,aAAO,EAAE,MAAM,SAAS,QAAQ,MAAM,aAAa,KAAA;AAAA,IACrD;AAAA,EAIF;AAGA,QAAM,UAAU,IAAI,SAAS,eAAe,OAAO;AACnD,QAAM,aAAkC,CAAA;AACxC,aAAW,KAAK,SAAS;AAGvB,eAAW,SAAS,EAAE,cAAc;AAClC,YAAM,OAAO,qBAAqB,OAAO,IAAI,kBAAkB,IAAI;AACnE,YAAM,SAAS,QAAQ;AACvB,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN;AAAA,QACA,aAAa,GAAG,EAAE,WAAW,KAAK,MAAM;AAAA,MAAA,CACzC;AAAA,IACH;AACA,eAAW,SAAS,EAAE,QAAQ;AAC5B,iBAAW,KAAK;AAAA,QACd,MAAM;AAAA,QACN,QAAQ,MAAM,YAAA;AAAA,QACd,aAAa,GAAG,EAAE,WAAW,KAAK,KAAK;AAAA,MAAA,CACxC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,WAAW,WAAW,GAAG;AAC3B,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,0CAA0C,OAAO;AAAA,IAAA;AAAA,EAE9D;AACA,MAAI,WAAW,WAAW,EAAG,QAAO,WAAW,CAAC;AAKhD,MAAI,QAAQ,WAAW,EAAG,QAAO,WAAW,CAAC;AAC7C,SAAO,EAAE,MAAM,aAAa,OAAO,SAAS,WAAA;AAC9C;AC7FO,SAAS,aAAa,UAAgD;AAC3E,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI,KAAK,SAAS,CAAC,EAAE;AACrB,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,QAAI,SAAS,CAAC,EAAE,KAAK,GAAI,MAAK,SAAS,CAAC,EAAE;AAAA,EAC5C;AACA,SAAO;AACT;ACnJA,SAAS,OAAO,MAAc,UAA0B;AACtD,QAAM,MAAM,QAAQ,IAAI,IAAI;AAC5B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,SAAS,KAAK,EAAE;AACtC,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,MAAM,uBAAuB,OAAO,6BAA6B,GAAK;AACtE,MAAM,qBAAqB,OAAO,2BAA2B,GAAG;AAChE,MAAM,qBAAqB,OAAO,2BAA2B,GAAM;AAKnE,MAAM,0BAA0B,OAAO,gCAAgC,GAAG;AAC1E,MAAM,+BAA+B,OAAO,qCAAqC,CAAC;AAElF,MAAM,mBAAmB,OAAO,yBAAyB,GAAM;AAC/D,MAAM,aAAa,OAAO,mBAAmB,IAAI;AACjD,MAAM,wBAAwB,OAAO,4BAA4B,EAAE;AAEnE,MAAM,wBAAwB,OAAO,yBAAyB,KAAK,KAAK,KAAK,GAAI;AACjF,MAAM,wBAAwB,OAAO,yBAAyB,KAAK,KAAK,GAAI;AAC5E,MAAM,gBAAgB,OAAO,sBAAsB,KAAK,KAAK,GAAI;AAoBjE,MAAM,QAAuB;AAAA,EAC3B,WAAW,KAAK,IAAA;AAAA,EAChB,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,yBAAyB;AAAA,EACzB,uBAAuB,KAAK,IAAA;AAAA,EAC5B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,aAAa,CAAA;AAAA,EACb,gBAAgB,KAAK,IAAA;AAAA,EACrB,YAAY;AACd;AAEA,IAAI,qBAA+C;AACnD,IAAI,iBAAwD;AAC5D,IAAI,cAAqD;AACzD,IAAI,YAAmD;AACvD,IAAI,YAAY;AAKT,SAAS,eAAqB;AACnC,QAAM,iBAAiB,KAAK,IAAA;AAC9B;AAGO,SAAS,oBAA6C;AAC3D,SAAO;AACT;AAIA,MAAM,2CAA2B,IAAA;AAO1B,SAAS,eAAe,IAAsC;AACnE,uBAAqB,IAAI,EAAE;AAC3B,SAAO,MAAM;AACX,yBAAqB,OAAO,EAAE;AAAA,EAChC;AACF;AAiBO,SAAS,gBAAgB,OAAqB,IAAU;AAC7D,MAAI,UAAW;AACf,cAAY;AACZ,QAAM,qBAAqB,KAAK,eAAe;AAG/C,uBAAqB,sBAAsB,EAAE,YAAY,GAAA,CAAI;AAC7D,qBAAmB,OAAA;AAEnB,QAAM,gBAAgB,QAAQ,IAAI,4BAA4B;AAE9D,mBAAiB,YAAY,MAAM;AACjC,QAAI,CAAC,sBAAsB,iBAAkB;AAQ7C,UAAM,MAAM,KAAK,IAAA;AACjB,UAAM,WAAW,MAAM,MAAM;AAC7B,UAAM,wBAAwB;AAC9B,QAAI,WAAW,IAAI,sBAAsB;AACvC,yBAAmB,MAAA;AACnB,YAAM,0BAA0B;AAChC,WAAK,kCAAkC;AAAA,QACrC,oBAAoB;AAAA,QACpB,sBAAsB;AAAA,MAAA,CACvB;AACD;AAAA,IACF;AAGA,UAAM,QAAQ,mBAAmB,WAAW,EAAE,IAAI;AAClD,UAAM,QAAQ,mBAAmB,MAAM;AACvC,UAAM,iBAAiB;AACvB,UAAM,iBAAiB;AACvB,uBAAmB,MAAA;AAOnB,QAAI,eAAe;AACjB,UAAI;AACF;AAAA,UACE;AAAA,UACA,KAAK,UAAU;AAAA,YACb,IAAI,KAAK,IAAA;AAAA,YACT,UAAU,KAAK,IAAA,IAAQ,MAAM;AAAA,YAC7B,gBAAgB,MAAM;AAAA,YACtB,gBAAgB,MAAM;AAAA,YACtB,yBAAyB,MAAM;AAAA,YAC/B,OAAO,MAAM;AAAA,YACb,QAAQ,MAAM;AAAA,YACd,YAAY,MAAM;AAAA,UAAA,CACnB;AAAA,QAAA;AAAA,MAEL,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI,SAAS,oBAAoB;AAC/B,kBAAY,sBAAsB;AAAA,QAChC,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,cAAc;AAAA,MAAA,CACf;AACD;AAAA,IACF;AAKA,QAAI,SAAS,yBAAyB;AACpC,YAAM,2BAA2B;AACjC,UAAI,MAAM,2BAA2B,8BAA8B;AACjE,oBAAY,4BAA4B;AAAA,UACtC,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,qBAAqB,MAAM;AAAA,UAC3B,oBAAoB;AAAA,UACpB,wBAAwB;AAAA,QAAA,CACzB;AACD;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,0BAA0B;AAAA,IAClC;AAEA,QAAI,SAAS,oBAAoB;AAC/B,WAAK,kBAAkB,EAAE,QAAQ,OAAO,QAAQ,OAAO,cAAc,oBAAoB;AAAA,IAC3F;AAAA,EACF,GAAG,oBAAoB;AACvB,iBAAe,MAAA;AAGf,gBAAc,YAAY,MAAM;AAC9B,QAAI,iBAAkB;AACtB,UAAM,KAAK,QAAQ,YAAA;AACnB,UAAM,QAAQ,OAAO,GAAG,MAAM,OAAO,IAAI;AACzC,UAAM,SAAS,OAAO,GAAG,WAAW,OAAO,IAAI;AAC/C,UAAM,QAAQ;AACd,UAAM,SAAS;AAGf,eAAW,MAAM,sBAAsB;AACrC,UAAI;AACF,WAAG,OAAO,MAAM;AAAA,MAClB,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,UAAM,YAAY,KAAK,MAAM;AAC7B,QAAI,MAAM,YAAY,SAAS,uBAAuB;AACpD,YAAM,YAAY,MAAA;AAAA,IACpB;AAEA,QAAI,SAAS,YAAY;AAMvB,UAAI;AACF,cAAM,2BAA2B;AAAA,UAC/B,YAAY,kBAAA;AAAA,UACZ,aAAa,uBAAA,EAAyB,IAAI,CAAC,QAAQ;AAAA,YACjD,MAAM,GAAG;AAAA,YACT,SAAS,OAAO,GAAG,kBAAkB,OAAO,IAAI;AAAA,UAAA,EAChD;AAAA,UACF,cAAc;AAAA,YACZ,QAAQ;AAAA,YACR,cAAc;AAAA,YACd,aAAa,OAAO,GAAG,WAAW,OAAO,IAAI;AAAA,YAC7C,kBAAkB,OAAO,GAAG,eAAe,OAAO,IAAI;AAAA,UAAA;AAAA,QACxD,CACD;AAAA,MACH,QAAQ;AAAA,MAER;AACA,kBAAY,gBAAgB,EAAE,QAAQ,OAAO,cAAc,YAAY;AACvE;AAAA,IACF;AAEA,QACE,MAAM,YAAY,UAAU,yBAC5B,uBAAuB,MAAM,WAAW,GACxC;AACA,kBAAY,yBAAyB;AAAA,QACnC,SAAS,MAAM,YAAY,MAAA;AAAA,QAC3B,oBAAoB;AAAA,MAAA,CACrB;AAAA,IACH;AAAA,EACF,GAAG,gBAAgB;AACnB,cAAY,MAAA;AAIZ,MAAI,oBAAoB;AACtB,gBAAY,YAAY,MAAM;AAC5B,UAAI,iBAAkB;AACtB,YAAM,WAAW,KAAK,IAAA,IAAQ,MAAM;AACpC,YAAM,SAAS,KAAK,IAAA,IAAQ,MAAM;AAClC,UAAI,YAAY,yBAAyB,UAAU,uBAAuB;AACxE,oBAAY,gBAAgB,EAAE,WAAW,UAAU,SAAS,QAAQ;AAAA,MACtE;AAAA,IACF,GAAG,aAAa;AAChB,cAAU,MAAA;AAAA,EACZ;AAEA,kBAAgB,MAAM;AACpB,QAAI,oBAAoB;AACtB,yBAAmB,QAAA;AACnB,2BAAqB;AAAA,IACvB;AACA,QAAI,8BAA8B,cAAc;AAChD,QAAI,2BAA2B,WAAW;AAC1C,QAAI,yBAAyB,SAAS;AAAA,EACxC,CAAC;AAED,OAAK,sBAAsB;AAAA,IACzB,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,yBAAyB;AAAA,IACzB,8BAA8B;AAAA,IAC9B,YAAY;AAAA,IACZ,uBAAuB;AAAA,IACvB,uBAAuB;AAAA,EAAA,CACxB;AACH;AAIA,SAAS,OAAO,GAAmB;AACjC,SAAO,KAAK,MAAM,IAAI,EAAE,IAAI;AAC9B;AAGO,SAAS,uBAAuB,SAA4B;AACjE,MAAI,QAAQ,SAAS,EAAG,QAAO;AAC/B,MAAI,OAAO,QAAQ,CAAC;AACpB,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,QAAI,QAAQ,CAAC,IAAI,KAAM,QAAO;AAC9B,WAAO,QAAQ,CAAC;AAAA,EAClB;AAGA,SAAO,QAAQ,QAAQ,SAAS,CAAC,IAAI,QAAQ,CAAC,KAAK;AACrD;AAEA,SAAS,YAAY,QAAgB,MAAqC;AACxE,MAAI,MAAM,WAAY;AACtB,QAAM,aAAa;AACnB,QAAM,kBAAkB,MAAM,IAAI,IAAI;AAGtC,aAAW,MAAM;AACf,UAAM,mDAAmD,EAAE,QAAQ;AACnE,YAAQ,KAAK,GAAG;AAAA,EAClB,GAAG,GAAK,EAAE,MAAA;AACV,WAAS,CAAC,EAAE,MAAM,MAAM,QAAQ,KAAK,CAAC,CAAC;AACzC;"}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imsg-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: humans
|
|
3
|
+
description: The humans/v1 convention — per-person relationship memory files for AI agents. Use when consulting, creating, or updating ~/.agents/humans/<person>.md files: before composing a message/email to someone, after significant exchanges with them, or when asked to summarize a relationship or communication history.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Humans files (humans/v1)
|
|
7
|
+
|
|
8
|
+
Agent-maintained markdown dossiers about the humans your user communicates
|
|
9
|
+
with — like `AGENTS.md`, but for people. One file per person at
|
|
10
|
+
`~/.agents/humans/<person-slug>.md`. Any agent or tool may read and write
|
|
11
|
+
these files directly; **imsg-mcp scaffolds them** (`init_human` tool /
|
|
12
|
+
`imsg humans init`) and feeds them message history, but the *calling agent*
|
|
13
|
+
does all summarization. Other data sources (email, calendars, CRMs) are
|
|
14
|
+
expected to contribute to the same files.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
When an agent is about to text your boss, it should first know: how does
|
|
19
|
+
this person prefer to be addressed? What has already been discussed? What
|
|
20
|
+
topics are off-limits? A humans file answers that in one read, without
|
|
21
|
+
re-ingesting years of history.
|
|
22
|
+
|
|
23
|
+
## File format
|
|
24
|
+
|
|
25
|
+
````markdown
|
|
26
|
+
---
|
|
27
|
+
schema: humans/v1
|
|
28
|
+
name: Sam Smith
|
|
29
|
+
aliases: [Dad, Sammy]
|
|
30
|
+
handles:
|
|
31
|
+
- "+61400000000"
|
|
32
|
+
- sam@example.com
|
|
33
|
+
first_contact: 2019-03-14
|
|
34
|
+
last_contact: 2026-07-10
|
|
35
|
+
sources: [imsg-mcp]
|
|
36
|
+
updated: 2026-07-12T10:00:00Z
|
|
37
|
+
privacy: never-share
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Summary
|
|
41
|
+
<= ~1500 chars. The "hot" section — always safe to inject into context.
|
|
42
|
+
|
|
43
|
+
## Facts
|
|
44
|
+
- Works at Acme as head of ops (imsg-mcp, 2026-07)
|
|
45
|
+
- Two kids, Alex and Jo (email-tool, 2025-11)
|
|
46
|
+
|
|
47
|
+
## Communication & Preferences
|
|
48
|
+
Preferred channel, response cadence, tone that lands well, best times.
|
|
49
|
+
|
|
50
|
+
## Boundaries
|
|
51
|
+
Hard constraints get their own section — never buried in prose.
|
|
52
|
+
|
|
53
|
+
## Timeline
|
|
54
|
+
Dated era entries (additive — append, don't rewrite).
|
|
55
|
+
|
|
56
|
+
## Current State (as of 2026-07)
|
|
57
|
+
Where things stand right now.
|
|
58
|
+
|
|
59
|
+
## Guidance
|
|
60
|
+
How to help the user interact well with this person.
|
|
61
|
+
|
|
62
|
+
## Log
|
|
63
|
+
- 2026-07-12: file created by imsg-mcp; 4,120 messages on record since 2019-03-14
|
|
64
|
+
- 2026-07-12: long call about the Q3 handover; he prefers texts before 8pm (imsg-mcp)
|
|
65
|
+
````
|
|
66
|
+
|
|
67
|
+
## Rules that make the format work
|
|
68
|
+
|
|
69
|
+
1. **`## Log` is append-only.** Any tool/agent may append a dated,
|
|
70
|
+
source-tagged entry. Never rewrite or truncate Log — it is the shared
|
|
71
|
+
ground truth that curation draws from.
|
|
72
|
+
2. **Curation is a separate, deliberate step.** Periodically distill Log
|
|
73
|
+
into the sections above (rewriting a section is fine — that's
|
|
74
|
+
distillation, not data loss, because Log persists). After distilling,
|
|
75
|
+
append a Log entry saying so.
|
|
76
|
+
3. **Tag facts with source + date** — `(imsg-mcp, 2026-07)` — so
|
|
77
|
+
conflicting sources can coexist and later be reconciled.
|
|
78
|
+
4. **Keep Summary within budget** (~1500 chars). It's what gets injected
|
|
79
|
+
when context is tight; the long tail lives below it.
|
|
80
|
+
5. **Additive across sources.** A new tool appends to `sources:` in the
|
|
81
|
+
frontmatter and to Log; it inserts Timeline entries in date order
|
|
82
|
+
rather than overwriting other tools' entries.
|
|
83
|
+
6. **Person slugs contain no `~`** — thread slugs (`name~svc~hash`) are a
|
|
84
|
+
different namespace.
|
|
85
|
+
|
|
86
|
+
## Security & privacy (non-negotiable)
|
|
87
|
+
|
|
88
|
+
- `privacy: never-share` means exactly that: **never** send file contents
|
|
89
|
+
over iMessage/email/any channel, never quote it to or near the person it
|
|
90
|
+
describes, and never load it into a conversation *with* that person or
|
|
91
|
+
any third party. Consult it silently; apply the guidance.
|
|
92
|
+
- Files are plaintext markdown **by design** — greppable, portable, editable
|
|
93
|
+
by any tool. At-rest protection is the OS's job (FileVault); secrets never
|
|
94
|
+
belong in these files. Keep permissions tight: files `600`, dir `700`
|
|
95
|
+
(the scaffolder does this for you).
|
|
96
|
+
- These files contain personal data about people who never consented to it.
|
|
97
|
+
They stay on this machine. Do not commit them, sync them to shared repos,
|
|
98
|
+
or include them in exports.
|
|
99
|
+
|
|
100
|
+
## Workflows
|
|
101
|
+
|
|
102
|
+
**Consult (before composing to someone):** read the person's file if it
|
|
103
|
+
exists — check Boundaries, Communication & Preferences, and Current State
|
|
104
|
+
before drafting. `list of people: ls ~/.agents/humans/`.
|
|
105
|
+
|
|
106
|
+
**Create (scaffold + first fill):**
|
|
107
|
+
1. `init_human {contact: "name-or-handle"}` (MCP) or `imsg humans init <contact>`
|
|
108
|
+
— creates the file with identity, handles, first/last contact and message
|
|
109
|
+
counts prefilled from the Address Book + chat.db. Never overwrites.
|
|
110
|
+
`init_human {top: 10}` scaffolds the user's top relationships (ranked by
|
|
111
|
+
the `relationship_leaderboard` analytic).
|
|
112
|
+
2. Export history: `export_messages {threadSlug, format: "markdown"}`.
|
|
113
|
+
3. Read the export, write the sections yourself (you are the summarizer),
|
|
114
|
+
append a provenance Log entry.
|
|
115
|
+
4. Delete the export file when done.
|
|
116
|
+
|
|
117
|
+
**Update (after significant exchanges):** append a dated Log entry; when
|
|
118
|
+
Log grows unwieldy, distill into sections (workflow rule 2).
|
|
119
|
+
|
|
120
|
+
## Discovery: tool-output hints
|
|
121
|
+
|
|
122
|
+
You don't have to remember these files exist. Once a file exists for a
|
|
123
|
+
person, imsg-mcp points at it wherever that person shows up:
|
|
124
|
+
|
|
125
|
+
- `get_messages` / `wait_for_reply` responses include a `humans` block —
|
|
126
|
+
the matching file path(s) (one per participant for group chats) plus
|
|
127
|
+
the standing guidance.
|
|
128
|
+
- `get_contact` includes `humansFile` (path or `null`); when no file
|
|
129
|
+
exists it suggests `init_human`.
|
|
130
|
+
- `list_conversations` rows carry `humansFiles` paths.
|
|
131
|
+
|
|
132
|
+
When you see the hint: read the file before composing; append notable
|
|
133
|
+
events to Log; if the exchange contains a major milestone or an important
|
|
134
|
+
relationship change, propose the Summary / Current State edit to the user
|
|
135
|
+
and wait for approval — Log appends are routine, section rewrites are
|
|
136
|
+
permission-gated.
|
package/skills/imsg-mcp/SKILL.md
CHANGED
|
@@ -171,3 +171,11 @@ Precedence: `.env` → `.env.local` → `.env.[mode]` → `.env.[mode].local`
|
|
|
171
171
|
- Never publish private message content.
|
|
172
172
|
- Confirm before sending messages on behalf of the user.
|
|
173
173
|
- See `AGENTS.md` for thread isolation and MCP guardrails.
|
|
174
|
+
|
|
175
|
+
## Humans files (relationship memory)
|
|
176
|
+
|
|
177
|
+
imsg-mcp scaffolds per-person relationship files at `~/.agents/humans/`
|
|
178
|
+
(`init_human` tool, `imsg humans init`, `imsg humans top`) following the
|
|
179
|
+
**humans/v1** convention — see `skills/humans/SKILL.md` for the format,
|
|
180
|
+
workflows, and privacy rules. The calling agent writes all summaries;
|
|
181
|
+
the tool only creates skeletons prefilled with identity + history stats.
|