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.
@@ -1,9 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, appendFileSync, mkdirSync, writeFileSync, unlinkSync, statSync, rmSync, copyFileSync } from "node:fs";
2
2
  import os, { homedir, freemem, tmpdir } from "node:os";
3
3
  import Database from "better-sqlite3";
4
4
  import { execFile } from "node:child_process";
5
5
  import { randomUUID, randomBytes, createHash } from "node:crypto";
6
- import { join, sep, dirname, resolve } from "node:path";
6
+ import { join, sep, dirname, resolve, basename } from "node:path";
7
7
  import { promisify } from "node:util";
8
8
  function resolveEnvPath(value, fallback) {
9
9
  if (!value) return fallback;
@@ -56,10 +56,14 @@ function getContactsDbPaths() {
56
56
  function getSlugsDbPath() {
57
57
  return resolveEnvPath(process.env.VITE_SLUGS_DB_PATH, join(homedir(), ".imsg-mcp", "slugs.db"));
58
58
  }
59
+ function getHumansDirPath() {
60
+ return resolveEnvPath(process.env.VITE_HUMANS_DIR, join(homedir(), ".agents", "humans"));
61
+ }
59
62
  const config = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
60
63
  __proto__: null,
61
64
  getContactsDbPaths,
62
65
  getEnv,
66
+ getHumansDirPath,
63
67
  getImsgDbPath,
64
68
  getSlugsDbPath,
65
69
  isAiEnv
@@ -502,10 +506,30 @@ async function sendMessageReliable(recipient, message, preferredService) {
502
506
  }
503
507
  }
504
508
  }
509
+ const ATTACHMENT_STAGING_DIR = join(homedir(), "Library", "Messages", "imsg-mcp-staging");
510
+ const STAGING_SWEEP_AGE_MS = 60 * 60 * 1e3;
511
+ function stageAttachmentForSend(filepath, stagingDir = ATTACHMENT_STAGING_DIR) {
512
+ try {
513
+ mkdirSync(stagingDir, { recursive: true });
514
+ for (const entry of readdirSync(stagingDir)) {
515
+ const p = join(stagingDir, entry);
516
+ try {
517
+ if (Date.now() - statSync(p).mtimeMs > STAGING_SWEEP_AGE_MS) rmSync(p, { force: true });
518
+ } catch {
519
+ }
520
+ }
521
+ const staged = join(stagingDir, `${randomBytes(4).toString("hex")}-${basename(filepath)}`);
522
+ copyFileSync(filepath, staged);
523
+ return staged;
524
+ } catch {
525
+ return filepath;
526
+ }
527
+ }
505
528
  async function sendAttachment(recipient, filepath, preferredService) {
506
529
  if (MOCK) return mockSend(`[attachment:${filepath}]`, { chatIdentifier: recipient });
530
+ const stagedPath = stageAttachmentForSend(filepath);
507
531
  const escapedRecipient = appleScriptEscape(recipient);
508
- const escapedPath = appleScriptEscape(filepath);
532
+ const escapedPath = appleScriptEscape(stagedPath);
509
533
  const script = buildParticipantSendScript({
510
534
  order: sendServiceOrder(recipient, preferredService),
511
535
  escapedRecipient,
@@ -731,6 +755,362 @@ function generateThreadSlug(input) {
731
755
  if (!namePart) namePart = "unknown";
732
756
  return `${namePart}~${svc}~${hash}`;
733
757
  }
758
+ const MS_PER_DAY = 864e5;
759
+ function localDateKey(d) {
760
+ const y = d.getFullYear();
761
+ const m = String(d.getMonth() + 1).padStart(2, "0");
762
+ const day = String(d.getDate()).padStart(2, "0");
763
+ return `${y}-${m}-${day}`;
764
+ }
765
+ function contactKey(m) {
766
+ return m.chatId;
767
+ }
768
+ function byDate(a, b) {
769
+ return a.date.getTime() - b.date.getTime();
770
+ }
771
+ function computeStreaks(messages) {
772
+ const byContact = /* @__PURE__ */ new Map();
773
+ for (const m of messages) {
774
+ if (m.isReaction) continue;
775
+ const c = contactKey(m);
776
+ if (!byContact.has(c)) byContact.set(c, /* @__PURE__ */ new Set());
777
+ byContact.get(c).add(localDateKey(m.date));
778
+ }
779
+ localDateKey(/* @__PURE__ */ new Date());
780
+ const results = [];
781
+ for (const [contact, dayset] of byContact) {
782
+ const days = Array.from(dayset).sort();
783
+ let longest = 0;
784
+ let longestStart = null;
785
+ let longestEnd = null;
786
+ let runLen = 0;
787
+ let runStart = null;
788
+ for (let i = 0; i < days.length; i++) {
789
+ if (i === 0 || new Date(days[i]).getTime() - new Date(days[i - 1]).getTime() !== MS_PER_DAY) {
790
+ runLen = 1;
791
+ runStart = days[i];
792
+ } else {
793
+ runLen++;
794
+ }
795
+ if (runLen > longest) {
796
+ longest = runLen;
797
+ longestStart = runStart;
798
+ longestEnd = days[i];
799
+ }
800
+ }
801
+ let curLen = 0;
802
+ const cursor = /* @__PURE__ */ new Date();
803
+ while (dayset.has(localDateKey(cursor))) {
804
+ curLen++;
805
+ cursor.setDate(cursor.getDate() - 1);
806
+ }
807
+ results.push({
808
+ contact,
809
+ longestStreakDays: longest,
810
+ longestStreakStart: longestStart,
811
+ longestStreakEnd: longestEnd,
812
+ currentStreakDays: curLen
813
+ });
814
+ }
815
+ results.sort((a, b) => b.longestStreakDays - a.longestStreakDays);
816
+ return results;
817
+ }
818
+ function computeDoubleTexts(messages) {
819
+ const byContact = /* @__PURE__ */ new Map();
820
+ for (const m of messages) {
821
+ if (m.isReaction) continue;
822
+ const c = contactKey(m);
823
+ if (!byContact.has(c)) byContact.set(c, []);
824
+ byContact.get(c).push(m);
825
+ }
826
+ const results = [];
827
+ for (const [contact, msgs] of byContact) {
828
+ msgs.sort(byDate);
829
+ let mineRun = 0;
830
+ let theirsRun = 0;
831
+ let mineDoubles = 0;
832
+ let theirsDoubles = 0;
833
+ for (const m of msgs) {
834
+ if (m.isFromMe) {
835
+ mineRun++;
836
+ theirsRun = 0;
837
+ if (mineRun >= 2) mineDoubles++;
838
+ } else {
839
+ theirsRun++;
840
+ mineRun = 0;
841
+ if (theirsRun >= 2) theirsDoubles++;
842
+ }
843
+ }
844
+ results.push({
845
+ contact,
846
+ doubleTextsFromMe: mineDoubles,
847
+ doubleTextsFromThem: theirsDoubles
848
+ });
849
+ }
850
+ results.sort(
851
+ (a, b) => b.doubleTextsFromMe + b.doubleTextsFromThem - (a.doubleTextsFromMe + a.doubleTextsFromThem)
852
+ );
853
+ return results;
854
+ }
855
+ function percentile(sorted, p) {
856
+ if (sorted.length === 0) return 0;
857
+ const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
858
+ return sorted[idx] ?? 0;
859
+ }
860
+ function computeResponseTimes(messages) {
861
+ const byContact = /* @__PURE__ */ new Map();
862
+ for (const m of messages) {
863
+ if (m.isReaction) continue;
864
+ const c = contactKey(m);
865
+ if (!byContact.has(c)) byContact.set(c, []);
866
+ byContact.get(c).push(m);
867
+ }
868
+ const results = [];
869
+ for (const [contact, msgs] of byContact) {
870
+ msgs.sort(byDate);
871
+ const deltas = [];
872
+ for (let i = 1; i < msgs.length; i++) {
873
+ const prev = msgs[i - 1];
874
+ const curr = msgs[i];
875
+ if (!prev.isFromMe && curr.isFromMe) {
876
+ deltas.push(curr.date.getTime() - prev.date.getTime());
877
+ }
878
+ }
879
+ if (deltas.length === 0) continue;
880
+ deltas.sort((a, b) => a - b);
881
+ const mean = deltas.reduce((s, n) => s + n, 0) / deltas.length;
882
+ results.push({
883
+ contact,
884
+ count: deltas.length,
885
+ medianMs: percentile(deltas, 50),
886
+ p95Ms: percentile(deltas, 95),
887
+ meanMs: Math.round(mean)
888
+ });
889
+ }
890
+ results.sort((a, b) => a.medianMs - b.medianMs);
891
+ return results;
892
+ }
893
+ function computeHeatmap(messages) {
894
+ const grid = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));
895
+ let total = 0;
896
+ for (const m of messages) {
897
+ if (m.isReaction) continue;
898
+ const dow = m.date.getDay();
899
+ const hr = m.date.getHours();
900
+ const dowRow = grid[dow];
901
+ if (dowRow && hr >= 0 && hr < 24) {
902
+ dowRow[hr] = (dowRow[hr] ?? 0) + 1;
903
+ total++;
904
+ }
905
+ }
906
+ return { grid, total };
907
+ }
908
+ function computeTapbacks(messages) {
909
+ const byContact = /* @__PURE__ */ new Map();
910
+ for (const m of messages) {
911
+ if (!m.isReaction || !m.reaction) continue;
912
+ const c = contactKey(m);
913
+ if (!byContact.has(c)) {
914
+ byContact.set(c, {
915
+ contact: c,
916
+ heart: 0,
917
+ thumbsUp: 0,
918
+ thumbsDown: 0,
919
+ haha: 0,
920
+ exclaim: 0,
921
+ question: 0,
922
+ emoji: 0,
923
+ total: 0
924
+ });
925
+ }
926
+ const r = byContact.get(c);
927
+ const t = (m.reaction.type || "").toLowerCase();
928
+ if (t.includes("heart") || t === "love") r.heart++;
929
+ else if (t.includes("thumb") && t.includes("up") || t === "like") r.thumbsUp++;
930
+ else if (t.includes("thumb") && t.includes("down") || t === "dislike") r.thumbsDown++;
931
+ else if (t.includes("haha") || t === "laugh") r.haha++;
932
+ else if (t.includes("exclaim") || t === "emphasis") r.exclaim++;
933
+ else if (t.includes("question")) r.question++;
934
+ else r.emoji++;
935
+ r.total++;
936
+ }
937
+ const out = Array.from(byContact.values());
938
+ out.sort((a, b) => b.total - a.total);
939
+ return out;
940
+ }
941
+ function computeWrapped(messages) {
942
+ if (messages.length === 0) {
943
+ return {
944
+ windowStart: "",
945
+ windowEnd: "",
946
+ totalSent: 0,
947
+ totalReceived: 0,
948
+ totalReactions: 0,
949
+ topContacts: [],
950
+ peakDay: null,
951
+ longestStreakDays: 0,
952
+ longestStreakContact: null
953
+ };
954
+ }
955
+ let sent = 0;
956
+ let received = 0;
957
+ let reactions = 0;
958
+ const perContact = /* @__PURE__ */ new Map();
959
+ const perDay = /* @__PURE__ */ new Map();
960
+ for (const m of messages) {
961
+ if (m.isReaction) {
962
+ reactions++;
963
+ continue;
964
+ }
965
+ if (m.isFromMe) sent++;
966
+ else received++;
967
+ const c = contactKey(m);
968
+ if (!perContact.has(c)) perContact.set(c, { sent: 0, received: 0, total: 0 });
969
+ const pc = perContact.get(c);
970
+ if (m.isFromMe) pc.sent++;
971
+ else pc.received++;
972
+ pc.total++;
973
+ const dk = localDateKey(m.date);
974
+ perDay.set(dk, (perDay.get(dk) ?? 0) + 1);
975
+ }
976
+ const topContacts = Array.from(perContact.entries()).map(([contact, v]) => ({ contact, ...v })).sort((a, b) => b.total - a.total).slice(0, 5);
977
+ let peakDay = null;
978
+ for (const [d, count] of perDay) {
979
+ if (!peakDay || count > peakDay.count) peakDay = { date: d, count };
980
+ }
981
+ const streaks = computeStreaks(messages);
982
+ const top = streaks[0];
983
+ return {
984
+ windowStart: localDateKey(messages[0].date),
985
+ windowEnd: localDateKey(messages[messages.length - 1].date),
986
+ totalSent: sent,
987
+ totalReceived: received,
988
+ totalReactions: reactions,
989
+ topContacts,
990
+ peakDay,
991
+ longestStreakDays: top?.longestStreakDays ?? 0,
992
+ longestStreakContact: top?.contact ?? null
993
+ };
994
+ }
995
+ function computeRelationshipLeaderboard(messages) {
996
+ const byChat = /* @__PURE__ */ new Map();
997
+ for (const m of messages) {
998
+ if (m.isReaction) continue;
999
+ if (!m.chatId || isGroupChatIdentifier(m.chatId)) continue;
1000
+ let acc = byChat.get(m.chatId);
1001
+ if (!acc) {
1002
+ acc = { contact: null, handle: m.chatId, sent: 0, received: 0, lastMs: 0 };
1003
+ byChat.set(m.chatId, acc);
1004
+ }
1005
+ if (m.isFromMe) {
1006
+ acc.sent++;
1007
+ } else {
1008
+ acc.received++;
1009
+ if (!acc.contact && m.displayName) acc.contact = m.displayName;
1010
+ if (m.handle && m.handle !== "unknown") acc.handle = m.handle;
1011
+ }
1012
+ if (m.date.getTime() > acc.lastMs) acc.lastMs = m.date.getTime();
1013
+ }
1014
+ const byContact = /* @__PURE__ */ new Map();
1015
+ for (const acc of byChat.values()) {
1016
+ const key = acc.contact ?? acc.handle;
1017
+ if (!key || key === "unknown") continue;
1018
+ const merged = byContact.get(key);
1019
+ if (merged) {
1020
+ merged.sent += acc.sent;
1021
+ merged.received += acc.received;
1022
+ merged.lastMs = Math.max(merged.lastMs, acc.lastMs);
1023
+ } else {
1024
+ byContact.set(key, { ...acc });
1025
+ }
1026
+ }
1027
+ const now = Date.now();
1028
+ const leaderboard = [];
1029
+ for (const [contact, acc] of byContact) {
1030
+ const total = acc.sent + acc.received;
1031
+ if (total < 3) continue;
1032
+ const reciprocity = Math.min(acc.sent, acc.received) / Math.max(Math.max(acc.sent, acc.received), 1);
1033
+ const daysSinceLast = Math.max(0, (now - acc.lastMs) / 864e5);
1034
+ const score = Math.log2(1 + total) * (0.4 + 0.6 * reciprocity) * Math.exp(-daysSinceLast / 45);
1035
+ leaderboard.push({
1036
+ contact,
1037
+ handle: acc.handle,
1038
+ total,
1039
+ sent: acc.sent,
1040
+ received: acc.received,
1041
+ reciprocity: Math.round(reciprocity * 100) / 100,
1042
+ daysSinceLast: Math.round(daysSinceLast * 10) / 10,
1043
+ score
1044
+ });
1045
+ }
1046
+ leaderboard.sort((a, b) => b.score - a.score);
1047
+ for (const row of leaderboard) row.score = Math.round(row.score * 1e6) / 1e6;
1048
+ return { leaderboard: leaderboard.slice(0, 50) };
1049
+ }
1050
+ const IMPLEMENTED_TYPES = [
1051
+ "messaging_streaks",
1052
+ "double_texts",
1053
+ "response_time_stats",
1054
+ "daily_heatmap",
1055
+ "tapback_summary",
1056
+ "year_in_review_wrapped",
1057
+ "relationship_leaderboard"
1058
+ ];
1059
+ const ANALYTIC_INFO = {
1060
+ messaging_streaks: {
1061
+ label: "Messaging Streaks",
1062
+ description: "Longest and current daily-message streaks per contact",
1063
+ defaultWindowDays: 365
1064
+ },
1065
+ double_texts: {
1066
+ label: "Double Texts",
1067
+ description: "Consecutive messages sent without a reply",
1068
+ defaultWindowDays: 90
1069
+ },
1070
+ response_time_stats: {
1071
+ label: "Response Times",
1072
+ description: "Reply latency per contact (median, p95, mean)",
1073
+ defaultWindowDays: 90
1074
+ },
1075
+ daily_heatmap: {
1076
+ label: "Daily Heatmap",
1077
+ description: "7×24 grid of activity by weekday × hour",
1078
+ defaultWindowDays: 90
1079
+ },
1080
+ tapback_summary: {
1081
+ label: "Tapback Summary",
1082
+ description: "Tapback reactions sent per contact",
1083
+ defaultWindowDays: 365
1084
+ },
1085
+ year_in_review_wrapped: {
1086
+ label: "Year in Review (Wrapped)",
1087
+ description: "Wrapped summary: top contacts, peak day, totals (pinned to 365d)",
1088
+ defaultWindowDays: 365
1089
+ },
1090
+ relationship_leaderboard: {
1091
+ label: "Relationship Leaderboard",
1092
+ description: "Top relationships by volume × reciprocity × recency",
1093
+ defaultWindowDays: 1825
1094
+ }
1095
+ };
1096
+ function dispatchAnalytic(type, messages) {
1097
+ switch (type) {
1098
+ case "messaging_streaks":
1099
+ return { type, data: computeStreaks(messages) };
1100
+ case "double_texts":
1101
+ return { type, data: computeDoubleTexts(messages) };
1102
+ case "response_time_stats":
1103
+ return { type, data: computeResponseTimes(messages) };
1104
+ case "daily_heatmap":
1105
+ return { type, data: computeHeatmap(messages) };
1106
+ case "tapback_summary":
1107
+ return { type, data: computeTapbacks(messages) };
1108
+ case "year_in_review_wrapped":
1109
+ return { type, data: computeWrapped(messages) };
1110
+ case "relationship_leaderboard":
1111
+ return { type, data: computeRelationshipLeaderboard(messages) };
1112
+ }
1113
+ }
734
1114
  const registry = /* @__PURE__ */ new Set();
735
1115
  let shuttingDown = false;
736
1116
  let watchdogTimer = null;
@@ -822,53 +1202,59 @@ function isShuttingDown() {
822
1202
  return shuttingDown;
823
1203
  }
824
1204
  export {
825
- logShutdown as A,
826
- stopHeapMonitor as B,
827
- getLogFilePath as C,
828
- getLastSendError as D,
829
- looksLikeThreadSlug as E,
830
- enableFileLogging as F,
831
- isShuttingDown as G,
832
- warn as H,
833
- error as I,
834
- isGroupGuid as J,
835
- isGroupChatIdentifier as K,
836
- generateThreadSlug as L,
837
- AssociatedMessageType as M,
838
- macAutoTimestampToDate as N,
839
- OBJECT_REPLACEMENT_CHAR as O,
840
- isReactionType as P,
841
- MAC_EPOCH_OFFSET as Q,
842
- NANOS_PER_SECOND as R,
843
- parseAssociatedMessageGuid as S,
1205
+ config as $,
1206
+ info as A,
1207
+ startHeapMonitor as B,
1208
+ getLogs as C,
1209
+ setLastSendError as D,
1210
+ logShutdown as E,
1211
+ stopHeapMonitor as F,
1212
+ getLogFilePath as G,
1213
+ getLastSendError as H,
1214
+ IMPLEMENTED_TYPES as I,
1215
+ ANALYTIC_INFO as J,
1216
+ looksLikeThreadSlug as K,
1217
+ enableFileLogging as L,
1218
+ isShuttingDown as M,
1219
+ warn as N,
1220
+ error as O,
1221
+ OBJECT_REPLACEMENT_CHAR as P,
1222
+ isGroupGuid as Q,
1223
+ isGroupChatIdentifier as R,
1224
+ generateThreadSlug as S,
844
1225
  Tables as T,
845
- macTimestampToDate as U,
846
- config as V,
847
- getContactsDbPaths as a,
848
- getSlugsDbPath as b,
1226
+ AssociatedMessageType as U,
1227
+ macAutoTimestampToDate as V,
1228
+ isReactionType as W,
1229
+ MAC_EPOCH_OFFSET as X,
1230
+ NANOS_PER_SECOND as Y,
1231
+ macTimestampToDate as Z,
1232
+ parseAssociatedMessageGuid as _,
1233
+ shutdown as a,
1234
+ getImsgDbPath as b,
849
1235
  checkLocalAccess as c,
850
- appendLog as d,
851
- getFileLogLines as e,
1236
+ getContactsDbPaths as d,
1237
+ getSlugsDbPath as e,
852
1238
  formatAccessReport as f,
853
- getImsgDbPath as g,
854
- getLogDirectory as h,
855
- checkMessagesAvailable as i,
856
- sendToChat as j,
857
- sendToChatId as k,
858
- sendMessageReliable as l,
859
- sendMessageAlt as m,
860
- sendAttachment as n,
861
- checkImessageAvailability as o,
1239
+ getHumansDirPath as g,
1240
+ appendLog as h,
1241
+ getFileLogLines as i,
1242
+ getLogDirectory as j,
1243
+ checkMessagesAvailable as k,
1244
+ sendToChat as l,
1245
+ sendToChatId as m,
1246
+ sendMessageReliable as n,
1247
+ sendMessageAlt as o,
862
1248
  perf as p,
863
- installShutdownHandlers as q,
864
- registerCleanup as r,
865
- shutdown as s,
866
- enableStdinEofDetection as t,
867
- enableOrphanWatchdog as u,
868
- logStartup as v,
869
- info as w,
870
- startHeapMonitor as x,
871
- getLogs as y,
872
- setLastSendError as z
1249
+ sendAttachment as q,
1250
+ checkImessageAvailability as r,
1251
+ sanitizeSlugPart as s,
1252
+ computeRelationshipLeaderboard as t,
1253
+ dispatchAnalytic as u,
1254
+ installShutdownHandlers as v,
1255
+ registerCleanup as w,
1256
+ enableStdinEofDetection as x,
1257
+ enableOrphanWatchdog as y,
1258
+ logStartup as z
873
1259
  };
874
- //# sourceMappingURL=shutdown-pPAPSnk3.js.map
1260
+ //# sourceMappingURL=shutdown-Vh0gjb6m.js.map