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.
@@ -4,7 +4,7 @@ import Database from "better-sqlite3";
4
4
  import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { existsSync, readdirSync, mkdirSync } from "node:fs";
7
- import { O as OBJECT_REPLACEMENT_CHAR, p as perf, J as isGroupGuid, K as isGroupChatIdentifier, L as generateThreadSlug, T as Tables, M as AssociatedMessageType, N as macAutoTimestampToDate, P as isReactionType, Q as MAC_EPOCH_OFFSET, R as NANOS_PER_SECOND, S as parseAssociatedMessageGuid$1, U as macTimestampToDate$1 } from "./shutdown-pPAPSnk3.js";
7
+ import { O as OBJECT_REPLACEMENT_CHAR, p as perf, M as isGroupGuid, I as isGroupChatIdentifier, N as generateThreadSlug, T as Tables, P as AssociatedMessageType, Q as macAutoTimestampToDate, R as isReactionType, S as MAC_EPOCH_OFFSET, U as NANOS_PER_SECOND, V as macTimestampToDate$1, W as parseAssociatedMessageGuid$1 } from "./shutdown-CqGQKFmr.js";
8
8
  import { parseBuffer } from "bplist-parser";
9
9
  function normalizePhoneNumber(phone) {
10
10
  const digits = phone.replace(/\D/g, "");
@@ -633,6 +633,8 @@ function getNativeParser() {
633
633
  }
634
634
  function nativeResultLooksTrustworthy(text) {
635
635
  if (/at_\d+_[0-9A-F-]{8,}/i.test(text)) return false;
636
+ if (/^.?[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(text)) return false;
637
+ if (/^.?at_[0-9A-F][0-9A-F_-]{19,}$/i.test(text)) return false;
636
638
  if (/__kIM[A-Z]/.test(text)) return false;
637
639
  if (/\$class|streamtyped|NSKeyedArchiver/.test(text)) return false;
638
640
  if (/^NS[A-Z][a-z]+/.test(text)) return false;
@@ -656,6 +658,13 @@ const METADATA_PATTERNS = [
656
658
  /^NSNumber$/,
657
659
  /^NSValue$/,
658
660
  /^at_\d+_[0-9A-F-]+$/i,
661
+ // Bare-UUID attribute values (sticker/attachment GUIDs), optionally with the
662
+ // leaked typedstream length byte in front ("$FE7B0D17-…", '$' = 0x24 = 36 =
663
+ // a UUID's length). No human message is exactly a bare UUID.
664
+ /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
665
+ /^.[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i,
666
+ // Transfer-GUID with leaked length byte ("Mat_BDA9FB97-…", 'M' = 77).
667
+ /^.at_[0-9A-F][0-9A-F_-]{19,}$/i,
659
668
  /FileTransferGUIDAttributeName/i,
660
669
  /BaseWritingDirectionAttributeName/i,
661
670
  /^\d{2}.*��/
@@ -707,23 +716,30 @@ function extractAttributedBodyText(blob) {
707
716
  if (score > prev) candidates.set(normalized, score);
708
717
  }
709
718
  };
719
+ let structuredTrusted = false;
710
720
  try {
711
721
  if (Date.now() < deadline) {
722
+ const strings = parser.parseAllNSStrings();
723
+ structuredTrusted = strings.some(
724
+ (entry) => normalizeCandidate(entry.content) !== null || /^[\uFFFC\s]+$/.test(entry.content)
725
+ );
712
726
  remember(
713
- parser.parseAllNSStrings().map((entry) => entry.content),
727
+ strings.map((entry) => entry.content),
714
728
  STRUCTURED_BOOST
715
729
  );
716
730
  }
717
731
  } catch {
718
732
  }
719
- try {
733
+ if (!structuredTrusted) {
734
+ try {
735
+ if (Date.now() < deadline) {
736
+ remember(parser.extractReadableText());
737
+ }
738
+ } catch {
739
+ }
720
740
  if (Date.now() < deadline) {
721
- remember(parser.extractReadableText());
741
+ remember(blob.toString("utf8").split(CONTROL_SPLIT_RE));
722
742
  }
723
- } catch {
724
- }
725
- if (Date.now() < deadline) {
726
- remember(blob.toString("utf8").split(CONTROL_SPLIT_RE));
727
743
  }
728
744
  const best = [...candidates.entries()].sort((a, b) => b[1] - a[1])[0];
729
745
  return best?.[0];
@@ -760,6 +776,10 @@ function normalizeText(value) {
760
776
  const normalized = value?.replace(/\s+/g, " ").trim();
761
777
  return normalized && normalized.length >= 4 ? normalized : void 0;
762
778
  }
779
+ function isPlausibleHumanText(s) {
780
+ const wordish = (s.match(/[\p{L}\p{N}\s]/gu) ?? []).length;
781
+ return wordish / s.length >= 0.7 && new RegExp("\\p{L}{2}", "u").test(s);
782
+ }
763
783
  function extractNullPaddedAsciiText(blob) {
764
784
  if (!blob) return void 0;
765
785
  let best = "";
@@ -781,7 +801,8 @@ function extractNullPaddedAsciiText(blob) {
781
801
  flush();
782
802
  }
783
803
  flush();
784
- return normalizeText(best);
804
+ const normalized = normalizeText(best);
805
+ return normalized && isPlausibleHumanText(normalized) ? normalized : void 0;
785
806
  }
786
807
  function extractArchivedAttributedStringText(blob) {
787
808
  if (!blob) return void 0;
@@ -1420,6 +1441,7 @@ class IMessageDB {
1420
1441
  m.is_read,
1421
1442
  m.date_read,
1422
1443
  m.is_delivered,
1444
+ m.error,
1423
1445
  m.date_delivered,
1424
1446
  m.associated_message_type,
1425
1447
  m.associated_message_guid,
@@ -1529,13 +1551,69 @@ class IMessageDB {
1529
1551
  const messages = await this.getMessagesForChat(chatIdentifier, 1);
1530
1552
  return messages.length > 0 ? messages[messages.length - 1] : null;
1531
1553
  }
1554
+ /**
1555
+ * The service the conversation ACTUALLY works on, judged by delivery
1556
+ * evidence: the most recent message that either came FROM the other party
1557
+ * (any service they use reaches us) or was sent by us WITHOUT error.
1558
+ * Failed sends are excluded — otherwise one wrong-service attempt (e.g. an
1559
+ * iMessage 'sent' to an SMS-only number, error 22) mints a phantom leg,
1560
+ * flips the thread's apparent service, and every later send repeats the
1561
+ * mistake. Returns null when the conversation has no usable evidence.
1562
+ */
1563
+ getPreferredSendService(chatIdentifier) {
1564
+ const chats = this.resolveChatsForConversation(chatIdentifier);
1565
+ if (chats.length === 0) return null;
1566
+ const placeholders = chats.map(() => "?").join(",");
1567
+ const row = this.raw.prepare(
1568
+ `SELECT m.service
1569
+ FROM ${Tables.MESSAGE} m
1570
+ JOIN ${Tables.CHAT_MESSAGE_JOIN} j ON j.message_id = m.ROWID
1571
+ WHERE j.chat_id IN (${placeholders})
1572
+ AND (m.associated_message_type = 0 OR m.associated_message_type IS NULL)
1573
+ AND (m.is_from_me = 0 OR m.error = 0 OR m.error IS NULL)
1574
+ AND m.service IN ('iMessage', 'SMS')
1575
+ ORDER BY m.date DESC, m.ROWID DESC
1576
+ LIMIT 1`
1577
+ ).get(...chats.map((c) => c.ROWID));
1578
+ if (row?.service === "SMS") return "SMS";
1579
+ if (row?.service === "iMessage") return "iMessage";
1580
+ return null;
1581
+ }
1582
+ /**
1583
+ * Aggregate stats over ALL merged legs of a conversation — one SQL
1584
+ * COUNT/MIN/MAX instead of materializing messages. Reactions and other
1585
+ * associated messages are excluded (normal items only). Used by
1586
+ * init_human to prefill first/last contact + volume.
1587
+ */
1588
+ getChatStats(chatIdentifier) {
1589
+ const chats = this.resolveChatsForConversation(chatIdentifier);
1590
+ if (chats.length === 0) return { count: 0, first: null, last: null };
1591
+ const placeholders = chats.map(() => "?").join(",");
1592
+ const row = this.raw.prepare(
1593
+ `SELECT COUNT(*) as count, MIN(m.date) as first, MAX(m.date) as last
1594
+ FROM ${Tables.MESSAGE} m
1595
+ JOIN ${Tables.CHAT_MESSAGE_JOIN} j ON j.message_id = m.ROWID
1596
+ WHERE j.chat_id IN (${placeholders})
1597
+ AND (m.associated_message_type = 0 OR m.associated_message_type IS NULL)`
1598
+ ).get(...chats.map((c) => c.ROWID));
1599
+ return {
1600
+ count: row.count ?? 0,
1601
+ first: row.first != null ? macTimestampToDate(row.first) : null,
1602
+ last: row.last != null ? macTimestampToDate(row.last) : null
1603
+ };
1604
+ }
1532
1605
  /**
1533
1606
  * Get messages after a specific message ID (for polling new messages).
1534
- * Returns incoming messages only, sorted by date ascending (chronological).
1607
+ * Sorted by date ascending (chronological). By default returns incoming
1608
+ * messages only; `includeSelf` also returns from-me rows (the user's own
1609
+ * interjections from other devices) — callers using it must suppress the
1610
+ * agent's own send echo themselves (see SentEchoRegistry).
1535
1611
  */
1536
- async getMessagesAfter(chatIdentifier, afterMessageId) {
1612
+ async getMessagesAfter(chatIdentifier, afterMessageId, options = {}) {
1537
1613
  const messages = await this.getMessagesForChat(chatIdentifier, 1e3, { afterMessageId });
1538
- const filtered = messages.filter((m) => m.id !== afterMessageId && !m.isFromMe);
1614
+ const filtered = messages.filter(
1615
+ (m) => m.id !== afterMessageId && (options.includeSelf || !m.isFromMe)
1616
+ );
1539
1617
  filtered.sort((a, b) => a.date.getTime() - b.date.getTime());
1540
1618
  return filtered;
1541
1619
  }
@@ -2230,6 +2308,10 @@ class IMessageDB {
2230
2308
  dateDelivered: macTimestampToDate(ext.date_delivered ?? null),
2231
2309
  isRead: ext.is_read != null ? Boolean(ext.is_read) : true,
2232
2310
  isDelivered: ext.is_delivered != null ? Boolean(ext.is_delivered) : true,
2311
+ // Send failure surface: chat.db sets error != 0 (and is_sent = 0) on
2312
+ // from-me messages that never went out — e.g. an iMessage attempt to an
2313
+ // SMS-only number. Messages.app shows "Not Delivered"; so must we.
2314
+ sendError: raw.is_from_me && ext.error ? ext.error : void 0,
2233
2315
  chatId,
2234
2316
  service: this.detectServiceForMessage(ext),
2235
2317
  isReaction,
@@ -2312,6 +2394,7 @@ class IMessageDB {
2312
2394
  m.is_read,
2313
2395
  m.date_read,
2314
2396
  m.is_delivered,
2397
+ m.error,
2315
2398
  m.date_delivered,
2316
2399
  h.id as handle_id,
2317
2400
  h.service as handle_service,
@@ -2348,7 +2431,8 @@ class IMessageDB {
2348
2431
  const stmt = this.raw.prepare(`
2349
2432
  SELECT
2350
2433
  m.ROWID as _rowid,
2351
- m.is_read, m.date_read, m.is_delivered, m.date_delivered,
2434
+ m.is_read, m.date_read, m.is_delivered,
2435
+ m.error, m.date_delivered,
2352
2436
  h.id as handle_id, h.service as handle_service,
2353
2437
  m.associated_message_type, m.associated_message_guid,
2354
2438
  m.associated_message_emoji, m.thread_originator_guid,
@@ -2381,7 +2465,8 @@ class IMessageDB {
2381
2465
  const cutoffNanos = Math.floor((cutoffMs / 1e3 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);
2382
2466
  const sql = `
2383
2467
  SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.date_read, m.date_delivered,
2384
- m.is_read, m.is_delivered, m.is_from_me, h.id as handle_id,
2468
+ m.is_read, m.is_delivered,
2469
+ m.error, m.is_from_me, h.id as handle_id,
2385
2470
  h.service as handle_service,
2386
2471
  m.cache_has_attachments, m.associated_message_type,
2387
2472
  m.associated_message_guid, m.associated_message_emoji,
@@ -2517,6 +2602,7 @@ class IMessageDB {
2517
2602
  fetchAttachments(messageRowId) {
2518
2603
  const stmt = this.raw.prepare(`
2519
2604
  SELECT
2605
+ a.ROWID as row_id,
2520
2606
  a.filename,
2521
2607
  a.mime_type,
2522
2608
  a.transfer_name,
@@ -2527,6 +2613,7 @@ class IMessageDB {
2527
2613
  `);
2528
2614
  const rows = stmt.all(messageRowId);
2529
2615
  return rows.map((r) => ({
2616
+ rowId: r.row_id,
2530
2617
  filename: r.filename || "",
2531
2618
  mimeType: r.mime_type,
2532
2619
  transferName: r.transfer_name,
@@ -2662,4 +2749,4 @@ export {
2662
2749
  imessageDb as i,
2663
2750
  normalizedPhoneVariants as n
2664
2751
  };
2665
- //# sourceMappingURL=imessage-db-Cz8MIIpG.js.map
2752
+ //# sourceMappingURL=imessage-db-C8WKHFQA.js.map