imsg-mcp 1.3.2 → 1.5.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 +44 -0
- package/README.md +3 -2
- package/dist/cli.js +126 -20
- package/dist/cli.js.map +1 -1
- package/dist/{imessage-db-Cz8MIIpG.js → imessage-db-BrHJPOq7.js} +147 -19
- package/dist/imessage-db-BrHJPOq7.js.map +1 -0
- package/dist/index.js +847 -73
- package/dist/index.js.map +1 -1
- package/dist/meta-E4VgVmPs.js +165 -0
- package/dist/meta-E4VgVmPs.js.map +1 -0
- package/dist/{setup-BuXuKAHA.js → setup-CysR5gId.js} +2 -2
- package/dist/{setup-BuXuKAHA.js.map → setup-CysR5gId.js.map} +1 -1
- package/dist/{shutdown-pPAPSnk3.js → shutdown-Vh0gjb6m.js} +434 -48
- package/dist/shutdown-Vh0gjb6m.js.map +1 -0
- package/dist/tui.js +226 -64
- package/dist/tui.js.map +1 -1
- package/dist/{watchdog-Cbgm050b.js → watchdog-B3T-dQT-.js} +21 -266
- package/dist/watchdog-B3T-dQT-.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 +29 -0
- package/dist/imessage-db-Cz8MIIpG.js.map +0 -1
- package/dist/meta-DzE7CSrZ.js +0 -7
- package/dist/meta-DzE7CSrZ.js.map +0 -1
- package/dist/shutdown-pPAPSnk3.js.map +0 -1
- package/dist/watchdog-Cbgm050b.js.map +0 -1
|
@@ -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 {
|
|
7
|
+
import { P as OBJECT_REPLACEMENT_CHAR, p as perf, Q as isGroupGuid, R as isGroupChatIdentifier, S as generateThreadSlug, T as Tables, U as AssociatedMessageType, V as macAutoTimestampToDate, W as isReactionType, X as MAC_EPOCH_OFFSET, Y as NANOS_PER_SECOND, Z as macTimestampToDate$1, _ as parseAssociatedMessageGuid$1 } from "./shutdown-Vh0gjb6m.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
|
-
|
|
727
|
+
strings.map((entry) => entry.content),
|
|
714
728
|
STRUCTURED_BOOST
|
|
715
729
|
);
|
|
716
730
|
}
|
|
717
731
|
} catch {
|
|
718
732
|
}
|
|
719
|
-
|
|
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(
|
|
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,15 @@ 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
|
+
if (wordish / s.length < 0.7 || !new RegExp("\\p{L}{2}", "u").test(s)) return false;
|
|
782
|
+
if (!/\s/.test(s.trim())) {
|
|
783
|
+
const core = s.replace(/^[^\p{L}\p{N}]+/u, "");
|
|
784
|
+
if (!new RegExp("^(\\p{Lu}?\\p{Ll}+|\\p{Lu}+|\\p{N}+)$", "u").test(core)) return false;
|
|
785
|
+
}
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
763
788
|
function extractNullPaddedAsciiText(blob) {
|
|
764
789
|
if (!blob) return void 0;
|
|
765
790
|
let best = "";
|
|
@@ -781,7 +806,8 @@ function extractNullPaddedAsciiText(blob) {
|
|
|
781
806
|
flush();
|
|
782
807
|
}
|
|
783
808
|
flush();
|
|
784
|
-
|
|
809
|
+
const normalized = normalizeText(best);
|
|
810
|
+
return normalized && isPlausibleHumanText(normalized) ? normalized : void 0;
|
|
785
811
|
}
|
|
786
812
|
function extractArchivedAttributedStringText(blob) {
|
|
787
813
|
if (!blob) return void 0;
|
|
@@ -816,7 +842,7 @@ function extractChatSummaryText(blob) {
|
|
|
816
842
|
}
|
|
817
843
|
} catch {
|
|
818
844
|
}
|
|
819
|
-
return
|
|
845
|
+
return void 0;
|
|
820
846
|
}
|
|
821
847
|
const DEFAULT_DIR = join(homedir(), ".imsg-mcp");
|
|
822
848
|
const DEFAULT_DB = join(DEFAULT_DIR, "slugs.db");
|
|
@@ -1420,6 +1446,7 @@ class IMessageDB {
|
|
|
1420
1446
|
m.is_read,
|
|
1421
1447
|
m.date_read,
|
|
1422
1448
|
m.is_delivered,
|
|
1449
|
+
m.error,
|
|
1423
1450
|
m.date_delivered,
|
|
1424
1451
|
m.associated_message_type,
|
|
1425
1452
|
m.associated_message_guid,
|
|
@@ -1529,13 +1556,73 @@ class IMessageDB {
|
|
|
1529
1556
|
const messages = await this.getMessagesForChat(chatIdentifier, 1);
|
|
1530
1557
|
return messages.length > 0 ? messages[messages.length - 1] : null;
|
|
1531
1558
|
}
|
|
1559
|
+
/**
|
|
1560
|
+
* The service the conversation ACTUALLY works on, judged by delivery
|
|
1561
|
+
* evidence: the most recent message that either came FROM the other party
|
|
1562
|
+
* (any service they use reaches us) or was sent by us WITHOUT error.
|
|
1563
|
+
* Failed sends are excluded — otherwise one wrong-service attempt (e.g. an
|
|
1564
|
+
* iMessage 'sent' to an SMS-only number, error 22) mints a phantom leg,
|
|
1565
|
+
* flips the thread's apparent service, and every later send repeats the
|
|
1566
|
+
* mistake. Returns null when the conversation has no usable evidence.
|
|
1567
|
+
*/
|
|
1568
|
+
getPreferredSendService(chatIdentifier) {
|
|
1569
|
+
const chats = this.resolveChatsForConversation(chatIdentifier);
|
|
1570
|
+
if (chats.length === 0) return null;
|
|
1571
|
+
const placeholders = chats.map(() => "?").join(",");
|
|
1572
|
+
const row = this.raw.prepare(
|
|
1573
|
+
`SELECT m.service
|
|
1574
|
+
FROM ${Tables.MESSAGE} m
|
|
1575
|
+
JOIN ${Tables.CHAT_MESSAGE_JOIN} j ON j.message_id = m.ROWID
|
|
1576
|
+
WHERE j.chat_id IN (${placeholders})
|
|
1577
|
+
AND (m.associated_message_type = 0 OR m.associated_message_type IS NULL)
|
|
1578
|
+
AND (m.is_from_me = 0 OR m.error = 0 OR m.error IS NULL)
|
|
1579
|
+
AND m.service IN ('iMessage', 'SMS')
|
|
1580
|
+
ORDER BY m.date DESC, m.ROWID DESC
|
|
1581
|
+
LIMIT 1`
|
|
1582
|
+
).get(...chats.map((c) => c.ROWID));
|
|
1583
|
+
if (row?.service === "SMS") return "SMS";
|
|
1584
|
+
if (row?.service === "iMessage") return "iMessage";
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Aggregate stats over ALL merged legs of a conversation — one SQL
|
|
1589
|
+
* COUNT/MIN/MAX instead of materializing messages. Reactions and other
|
|
1590
|
+
* associated messages are excluded (normal items only). Used by
|
|
1591
|
+
* init_human to prefill first/last contact + volume.
|
|
1592
|
+
*/
|
|
1593
|
+
getChatStats(chatIdentifier) {
|
|
1594
|
+
const chats = this.resolveChatsForConversation(chatIdentifier);
|
|
1595
|
+
if (chats.length === 0) return { count: 0, first: null, last: null };
|
|
1596
|
+
const placeholders = chats.map(() => "?").join(",");
|
|
1597
|
+
const row = this.raw.prepare(
|
|
1598
|
+
// COUNT(DISTINCT m.ROWID): a single message is often joined to more
|
|
1599
|
+
// than one leg of a merged identity (Messages.app links it into both
|
|
1600
|
+
// the iMessage and SMS chat rows). Plain COUNT(*) counted it once per
|
|
1601
|
+
// leg and inflated the humans-file stats (e.g. +727 for one contact).
|
|
1602
|
+
`SELECT COUNT(DISTINCT m.ROWID) as count, MIN(m.date) as first, MAX(m.date) as last
|
|
1603
|
+
FROM ${Tables.MESSAGE} m
|
|
1604
|
+
JOIN ${Tables.CHAT_MESSAGE_JOIN} j ON j.message_id = m.ROWID
|
|
1605
|
+
WHERE j.chat_id IN (${placeholders})
|
|
1606
|
+
AND (m.associated_message_type = 0 OR m.associated_message_type IS NULL)`
|
|
1607
|
+
).get(...chats.map((c) => c.ROWID));
|
|
1608
|
+
return {
|
|
1609
|
+
count: row.count ?? 0,
|
|
1610
|
+
first: row.first != null ? macTimestampToDate(row.first) : null,
|
|
1611
|
+
last: row.last != null ? macTimestampToDate(row.last) : null
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1532
1614
|
/**
|
|
1533
1615
|
* Get messages after a specific message ID (for polling new messages).
|
|
1534
|
-
*
|
|
1616
|
+
* Sorted by date ascending (chronological). By default returns incoming
|
|
1617
|
+
* messages only; `includeSelf` also returns from-me rows (the user's own
|
|
1618
|
+
* interjections from other devices) — callers using it must suppress the
|
|
1619
|
+
* agent's own send echo themselves (see SentEchoRegistry).
|
|
1535
1620
|
*/
|
|
1536
|
-
async getMessagesAfter(chatIdentifier, afterMessageId) {
|
|
1621
|
+
async getMessagesAfter(chatIdentifier, afterMessageId, options = {}) {
|
|
1537
1622
|
const messages = await this.getMessagesForChat(chatIdentifier, 1e3, { afterMessageId });
|
|
1538
|
-
const filtered = messages.filter(
|
|
1623
|
+
const filtered = messages.filter(
|
|
1624
|
+
(m) => m.id !== afterMessageId && (options.includeSelf || !m.isFromMe)
|
|
1625
|
+
);
|
|
1539
1626
|
filtered.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1540
1627
|
return filtered;
|
|
1541
1628
|
}
|
|
@@ -1752,6 +1839,9 @@ class IMessageDB {
|
|
|
1752
1839
|
if (result == null) {
|
|
1753
1840
|
result = this.getPreviousConversationSnippet(last);
|
|
1754
1841
|
}
|
|
1842
|
+
if (result == null) {
|
|
1843
|
+
result = this.getLastTextfulSnippet(last);
|
|
1844
|
+
}
|
|
1755
1845
|
if (result == null) {
|
|
1756
1846
|
result = pickConversationSnippet({
|
|
1757
1847
|
summaryText: extractChatSummaryText(last.chatProperties) ?? null
|
|
@@ -1760,6 +1850,35 @@ class IMessageDB {
|
|
|
1760
1850
|
this.cachedSnippets.set(last.lastMessageId, result);
|
|
1761
1851
|
return result;
|
|
1762
1852
|
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Most recent message in the chat (any sender) with real extractable text,
|
|
1855
|
+
* looking back from the current last message. Unlike
|
|
1856
|
+
* getPreviousConversationSnippet this does NOT stop at a sender change or a
|
|
1857
|
+
* time gap — it exists for the "last message is a text-less tombstone"
|
|
1858
|
+
* (unsent/retracted) case, where the useful preview is simply the previous
|
|
1859
|
+
* real message.
|
|
1860
|
+
*/
|
|
1861
|
+
getLastTextfulSnippet(last) {
|
|
1862
|
+
const rows = this.raw.prepare(`
|
|
1863
|
+
SELECT m.text, m.attributedBody, m.ROWID
|
|
1864
|
+
FROM ${Tables.MESSAGE} m
|
|
1865
|
+
JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON cmj.message_id = m.ROWID
|
|
1866
|
+
WHERE cmj.chat_id = ?
|
|
1867
|
+
AND m.associated_message_type = ${AssociatedMessageType.NORMAL}
|
|
1868
|
+
AND COALESCE(m.item_type, 0) = 0
|
|
1869
|
+
AND (m.date < ? OR (m.date = ? AND m.ROWID < ?))
|
|
1870
|
+
ORDER BY m.date DESC, m.ROWID DESC
|
|
1871
|
+
LIMIT 10
|
|
1872
|
+
`).all(last.chatId, last.lastDate, last.lastDate, last.lastMessageId);
|
|
1873
|
+
for (const row of rows) {
|
|
1874
|
+
const snippet = pickConversationSnippet({
|
|
1875
|
+
rawText: row.text,
|
|
1876
|
+
parsedText: this.extractMessageText(row)
|
|
1877
|
+
});
|
|
1878
|
+
if (snippet && !isMetadataOnlySnippet(snippet)) return snippet;
|
|
1879
|
+
}
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1763
1882
|
shouldFallbackToPreviousSnippet(snippet, last) {
|
|
1764
1883
|
return Boolean(
|
|
1765
1884
|
last.balloonBundleId?.includes("URLBalloonProvider") && isMetadataOnlySnippet(snippet)
|
|
@@ -2230,6 +2349,10 @@ class IMessageDB {
|
|
|
2230
2349
|
dateDelivered: macTimestampToDate(ext.date_delivered ?? null),
|
|
2231
2350
|
isRead: ext.is_read != null ? Boolean(ext.is_read) : true,
|
|
2232
2351
|
isDelivered: ext.is_delivered != null ? Boolean(ext.is_delivered) : true,
|
|
2352
|
+
// Send failure surface: chat.db sets error != 0 (and is_sent = 0) on
|
|
2353
|
+
// from-me messages that never went out — e.g. an iMessage attempt to an
|
|
2354
|
+
// SMS-only number. Messages.app shows "Not Delivered"; so must we.
|
|
2355
|
+
sendError: raw.is_from_me && ext.error ? ext.error : void 0,
|
|
2233
2356
|
chatId,
|
|
2234
2357
|
service: this.detectServiceForMessage(ext),
|
|
2235
2358
|
isReaction,
|
|
@@ -2312,6 +2435,7 @@ class IMessageDB {
|
|
|
2312
2435
|
m.is_read,
|
|
2313
2436
|
m.date_read,
|
|
2314
2437
|
m.is_delivered,
|
|
2438
|
+
m.error,
|
|
2315
2439
|
m.date_delivered,
|
|
2316
2440
|
h.id as handle_id,
|
|
2317
2441
|
h.service as handle_service,
|
|
@@ -2348,7 +2472,8 @@ class IMessageDB {
|
|
|
2348
2472
|
const stmt = this.raw.prepare(`
|
|
2349
2473
|
SELECT
|
|
2350
2474
|
m.ROWID as _rowid,
|
|
2351
|
-
m.is_read, m.date_read, m.is_delivered,
|
|
2475
|
+
m.is_read, m.date_read, m.is_delivered,
|
|
2476
|
+
m.error, m.date_delivered,
|
|
2352
2477
|
h.id as handle_id, h.service as handle_service,
|
|
2353
2478
|
m.associated_message_type, m.associated_message_guid,
|
|
2354
2479
|
m.associated_message_emoji, m.thread_originator_guid,
|
|
@@ -2376,12 +2501,13 @@ class IMessageDB {
|
|
|
2376
2501
|
* chat_analytics tool — bounded by date, not by per-chat limit. Reactions
|
|
2377
2502
|
* are included (tapback analytics need them); hidden system items dropped.
|
|
2378
2503
|
*/
|
|
2379
|
-
async getMessagesInWindow(cutoffMs, capPerWindow =
|
|
2504
|
+
async getMessagesInWindow(cutoffMs, capPerWindow = 8e4) {
|
|
2380
2505
|
const span = perf("getMessagesInWindow");
|
|
2381
2506
|
const cutoffNanos = Math.floor((cutoffMs / 1e3 - MAC_EPOCH_OFFSET) * NANOS_PER_SECOND);
|
|
2382
2507
|
const sql = `
|
|
2383
2508
|
SELECT m.ROWID, m.guid, m.text, m.attributedBody, m.date, m.date_read, m.date_delivered,
|
|
2384
|
-
m.is_read, m.is_delivered,
|
|
2509
|
+
m.is_read, m.is_delivered,
|
|
2510
|
+
m.error, m.is_from_me, h.id as handle_id,
|
|
2385
2511
|
h.service as handle_service,
|
|
2386
2512
|
m.cache_has_attachments, m.associated_message_type,
|
|
2387
2513
|
m.associated_message_guid, m.associated_message_emoji,
|
|
@@ -2394,10 +2520,10 @@ class IMessageDB {
|
|
|
2394
2520
|
LEFT JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON m.ROWID = cmj.message_id
|
|
2395
2521
|
LEFT JOIN ${Tables.CHAT} c ON cmj.chat_id = c.ROWID
|
|
2396
2522
|
WHERE m.date >= ?
|
|
2397
|
-
ORDER BY m.date
|
|
2523
|
+
ORDER BY m.date DESC
|
|
2398
2524
|
LIMIT ?
|
|
2399
2525
|
`;
|
|
2400
|
-
const rows = this.raw.prepare(sql).all(cutoffNanos, capPerWindow);
|
|
2526
|
+
const rows = this.raw.prepare(sql).all(cutoffNanos, capPerWindow).reverse();
|
|
2401
2527
|
const out = [];
|
|
2402
2528
|
for (const r of rows) {
|
|
2403
2529
|
if (isHiddenSystemItem(r.item_type)) continue;
|
|
@@ -2517,6 +2643,7 @@ class IMessageDB {
|
|
|
2517
2643
|
fetchAttachments(messageRowId) {
|
|
2518
2644
|
const stmt = this.raw.prepare(`
|
|
2519
2645
|
SELECT
|
|
2646
|
+
a.ROWID as row_id,
|
|
2520
2647
|
a.filename,
|
|
2521
2648
|
a.mime_type,
|
|
2522
2649
|
a.transfer_name,
|
|
@@ -2527,6 +2654,7 @@ class IMessageDB {
|
|
|
2527
2654
|
`);
|
|
2528
2655
|
const rows = stmt.all(messageRowId);
|
|
2529
2656
|
return rows.map((r) => ({
|
|
2657
|
+
rowId: r.row_id,
|
|
2530
2658
|
filename: r.filename || "",
|
|
2531
2659
|
mimeType: r.mime_type,
|
|
2532
2660
|
transferName: r.transfer_name,
|
|
@@ -2662,4 +2790,4 @@ export {
|
|
|
2662
2790
|
imessageDb as i,
|
|
2663
2791
|
normalizedPhoneVariants as n
|
|
2664
2792
|
};
|
|
2665
|
-
//# sourceMappingURL=imessage-db-
|
|
2793
|
+
//# sourceMappingURL=imessage-db-BrHJPOq7.js.map
|