imsg-mcp 1.1.1 → 1.2.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 +12 -0
- package/dist/index.js +1 -329
- package/dist/index.js.map +1 -1
- package/dist/tui.js +1239 -47
- package/dist/tui.js.map +1 -1
- package/dist/{watchdog-pMKk8IGO.js → watchdog-0RGOHpj4.js} +341 -2
- package/dist/watchdog-0RGOHpj4.js.map +1 -0
- package/package.json +1 -1
- package/dist/watchdog-pMKk8IGO.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
# [1.2.0](https://github.com/george43g/imsg-mcp/compare/v1.1.1...v1.2.0) (2026-06-07)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **tui:** module-pane Tab cycles type; defer compute so Loading paints ([a69539a](https://github.com/george43g/imsg-mcp/commit/a69539ae6271dc6623dcfe9663728a760cc4fade))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **tui:** command palette + feature module system ([59d5628](https://github.com/george43g/imsg-mcp/commit/59d5628238fefc65d6806ad9969716fa508ea2c7))
|
|
17
|
+
|
|
6
18
|
## [1.1.1](https://github.com/george43g/imsg-mcp/compare/v1.1.0...v1.1.1) (2026-06-07)
|
|
7
19
|
|
|
8
20
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { c as checkLocalAccess, f as formatAccessReport, s as shutdown, g as getImsgDbPath, a as getContactsDbPaths, b as getSlugsDbPath, d as appendLog, e as getFileLogLines, h as getLogDirectory, p as perf, i as checkMessagesAvailable, j as sendToChat, k as sendToChatId, l as sendMessageReliable, m as sendMessageAlt, n as sendAttachment, o as checkImessageAvailability, q as installShutdownHandlers, r as registerCleanup, t as enableStdinEofDetection, u as enableOrphanWatchdog, v as logStartup, w as info, x as startHeapMonitor, y as getLogs, z as setLastSendError, A as logShutdown, B as stopHeapMonitor, C as getLogFilePath, D as getLastSendError } from "./shutdown-BJifUGdo.js";
|
|
10
|
+
import { r as resolveRecipient, d as defaultCountryFromEnv, a as rankFuzzy, b as dispatchAnalytic, i as installWatchdog, n as noteActivity, c as readWatchdogState } from "./watchdog-0RGOHpj4.js";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import Database from "better-sqlite3";
|
|
12
13
|
import { streamExport } from "./exportStream-Bu5recJ4.js";
|
|
@@ -14,262 +15,8 @@ import { I as IMessageDB, h as hasNativeModule } from "./imessage-db-BYU5mxM-.js
|
|
|
14
15
|
import { z } from "zod";
|
|
15
16
|
import { A as APP_VERSION, a as APP_NAME } from "./meta-D3NoTAjA.js";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
17
|
-
import { r as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog, n as noteActivity, a as readWatchdogState } from "./watchdog-pMKk8IGO.js";
|
|
18
18
|
import { parseUserDate } from "./dateParse-DJXMfq3a.js";
|
|
19
19
|
import { m as minMessageId } from "./types-Bbi1xtl-.js";
|
|
20
|
-
const MS_PER_DAY = 864e5;
|
|
21
|
-
function localDateKey(d) {
|
|
22
|
-
const y = d.getFullYear();
|
|
23
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
24
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
25
|
-
return `${y}-${m}-${day}`;
|
|
26
|
-
}
|
|
27
|
-
function contactKey(m) {
|
|
28
|
-
return m.chatId;
|
|
29
|
-
}
|
|
30
|
-
function byDate(a, b) {
|
|
31
|
-
return a.date.getTime() - b.date.getTime();
|
|
32
|
-
}
|
|
33
|
-
function computeStreaks(messages) {
|
|
34
|
-
const byContact = /* @__PURE__ */ new Map();
|
|
35
|
-
for (const m of messages) {
|
|
36
|
-
if (m.isReaction) continue;
|
|
37
|
-
const c = contactKey(m);
|
|
38
|
-
if (!byContact.has(c)) byContact.set(c, /* @__PURE__ */ new Set());
|
|
39
|
-
byContact.get(c).add(localDateKey(m.date));
|
|
40
|
-
}
|
|
41
|
-
localDateKey(/* @__PURE__ */ new Date());
|
|
42
|
-
const results = [];
|
|
43
|
-
for (const [contact, dayset] of byContact) {
|
|
44
|
-
const days = Array.from(dayset).sort();
|
|
45
|
-
let longest = 0;
|
|
46
|
-
let longestStart = null;
|
|
47
|
-
let longestEnd = null;
|
|
48
|
-
let runLen = 0;
|
|
49
|
-
let runStart = null;
|
|
50
|
-
for (let i = 0; i < days.length; i++) {
|
|
51
|
-
if (i === 0 || new Date(days[i]).getTime() - new Date(days[i - 1]).getTime() !== MS_PER_DAY) {
|
|
52
|
-
runLen = 1;
|
|
53
|
-
runStart = days[i];
|
|
54
|
-
} else {
|
|
55
|
-
runLen++;
|
|
56
|
-
}
|
|
57
|
-
if (runLen > longest) {
|
|
58
|
-
longest = runLen;
|
|
59
|
-
longestStart = runStart;
|
|
60
|
-
longestEnd = days[i];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
let curLen = 0;
|
|
64
|
-
const cursor = /* @__PURE__ */ new Date();
|
|
65
|
-
while (dayset.has(localDateKey(cursor))) {
|
|
66
|
-
curLen++;
|
|
67
|
-
cursor.setDate(cursor.getDate() - 1);
|
|
68
|
-
}
|
|
69
|
-
results.push({
|
|
70
|
-
contact,
|
|
71
|
-
longestStreakDays: longest,
|
|
72
|
-
longestStreakStart: longestStart,
|
|
73
|
-
longestStreakEnd: longestEnd,
|
|
74
|
-
currentStreakDays: curLen
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
results.sort((a, b) => b.longestStreakDays - a.longestStreakDays);
|
|
78
|
-
return results;
|
|
79
|
-
}
|
|
80
|
-
function computeDoubleTexts(messages) {
|
|
81
|
-
const byContact = /* @__PURE__ */ new Map();
|
|
82
|
-
for (const m of messages) {
|
|
83
|
-
if (m.isReaction) continue;
|
|
84
|
-
const c = contactKey(m);
|
|
85
|
-
if (!byContact.has(c)) byContact.set(c, []);
|
|
86
|
-
byContact.get(c).push(m);
|
|
87
|
-
}
|
|
88
|
-
const results = [];
|
|
89
|
-
for (const [contact, msgs] of byContact) {
|
|
90
|
-
msgs.sort(byDate);
|
|
91
|
-
let mineRun = 0;
|
|
92
|
-
let theirsRun = 0;
|
|
93
|
-
let mineDoubles = 0;
|
|
94
|
-
let theirsDoubles = 0;
|
|
95
|
-
for (const m of msgs) {
|
|
96
|
-
if (m.isFromMe) {
|
|
97
|
-
mineRun++;
|
|
98
|
-
theirsRun = 0;
|
|
99
|
-
if (mineRun >= 2) mineDoubles++;
|
|
100
|
-
} else {
|
|
101
|
-
theirsRun++;
|
|
102
|
-
mineRun = 0;
|
|
103
|
-
if (theirsRun >= 2) theirsDoubles++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
results.push({
|
|
107
|
-
contact,
|
|
108
|
-
doubleTextsFromMe: mineDoubles,
|
|
109
|
-
doubleTextsFromThem: theirsDoubles
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
results.sort(
|
|
113
|
-
(a, b) => b.doubleTextsFromMe + b.doubleTextsFromThem - (a.doubleTextsFromMe + a.doubleTextsFromThem)
|
|
114
|
-
);
|
|
115
|
-
return results;
|
|
116
|
-
}
|
|
117
|
-
function percentile(sorted, p) {
|
|
118
|
-
if (sorted.length === 0) return 0;
|
|
119
|
-
const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
|
|
120
|
-
return sorted[idx] ?? 0;
|
|
121
|
-
}
|
|
122
|
-
function computeResponseTimes(messages) {
|
|
123
|
-
const byContact = /* @__PURE__ */ new Map();
|
|
124
|
-
for (const m of messages) {
|
|
125
|
-
if (m.isReaction) continue;
|
|
126
|
-
const c = contactKey(m);
|
|
127
|
-
if (!byContact.has(c)) byContact.set(c, []);
|
|
128
|
-
byContact.get(c).push(m);
|
|
129
|
-
}
|
|
130
|
-
const results = [];
|
|
131
|
-
for (const [contact, msgs] of byContact) {
|
|
132
|
-
msgs.sort(byDate);
|
|
133
|
-
const deltas = [];
|
|
134
|
-
for (let i = 1; i < msgs.length; i++) {
|
|
135
|
-
const prev = msgs[i - 1];
|
|
136
|
-
const curr = msgs[i];
|
|
137
|
-
if (!prev.isFromMe && curr.isFromMe) {
|
|
138
|
-
deltas.push(curr.date.getTime() - prev.date.getTime());
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (deltas.length === 0) continue;
|
|
142
|
-
deltas.sort((a, b) => a - b);
|
|
143
|
-
const mean = deltas.reduce((s, n) => s + n, 0) / deltas.length;
|
|
144
|
-
results.push({
|
|
145
|
-
contact,
|
|
146
|
-
count: deltas.length,
|
|
147
|
-
medianMs: percentile(deltas, 50),
|
|
148
|
-
p95Ms: percentile(deltas, 95),
|
|
149
|
-
meanMs: Math.round(mean)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
results.sort((a, b) => a.medianMs - b.medianMs);
|
|
153
|
-
return results;
|
|
154
|
-
}
|
|
155
|
-
function computeHeatmap(messages) {
|
|
156
|
-
const grid = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));
|
|
157
|
-
let total = 0;
|
|
158
|
-
for (const m of messages) {
|
|
159
|
-
if (m.isReaction) continue;
|
|
160
|
-
const dow = m.date.getDay();
|
|
161
|
-
const hr = m.date.getHours();
|
|
162
|
-
const dowRow = grid[dow];
|
|
163
|
-
if (dowRow && hr >= 0 && hr < 24) {
|
|
164
|
-
dowRow[hr] = (dowRow[hr] ?? 0) + 1;
|
|
165
|
-
total++;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return { grid, total };
|
|
169
|
-
}
|
|
170
|
-
function computeTapbacks(messages) {
|
|
171
|
-
const byContact = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const m of messages) {
|
|
173
|
-
if (!m.isReaction || !m.reaction) continue;
|
|
174
|
-
const c = contactKey(m);
|
|
175
|
-
if (!byContact.has(c)) {
|
|
176
|
-
byContact.set(c, {
|
|
177
|
-
contact: c,
|
|
178
|
-
heart: 0,
|
|
179
|
-
thumbsUp: 0,
|
|
180
|
-
thumbsDown: 0,
|
|
181
|
-
haha: 0,
|
|
182
|
-
exclaim: 0,
|
|
183
|
-
question: 0,
|
|
184
|
-
emoji: 0,
|
|
185
|
-
total: 0
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
const r = byContact.get(c);
|
|
189
|
-
const t = (m.reaction.type || "").toLowerCase();
|
|
190
|
-
if (t.includes("heart") || t === "love") r.heart++;
|
|
191
|
-
else if (t.includes("thumb") && t.includes("up") || t === "like") r.thumbsUp++;
|
|
192
|
-
else if (t.includes("thumb") && t.includes("down") || t === "dislike") r.thumbsDown++;
|
|
193
|
-
else if (t.includes("haha") || t === "laugh") r.haha++;
|
|
194
|
-
else if (t.includes("exclaim") || t === "emphasis") r.exclaim++;
|
|
195
|
-
else if (t.includes("question")) r.question++;
|
|
196
|
-
else r.emoji++;
|
|
197
|
-
r.total++;
|
|
198
|
-
}
|
|
199
|
-
const out = Array.from(byContact.values());
|
|
200
|
-
out.sort((a, b) => b.total - a.total);
|
|
201
|
-
return out;
|
|
202
|
-
}
|
|
203
|
-
function computeWrapped(messages) {
|
|
204
|
-
if (messages.length === 0) {
|
|
205
|
-
return {
|
|
206
|
-
windowStart: "",
|
|
207
|
-
windowEnd: "",
|
|
208
|
-
totalSent: 0,
|
|
209
|
-
totalReceived: 0,
|
|
210
|
-
totalReactions: 0,
|
|
211
|
-
topContacts: [],
|
|
212
|
-
peakDay: null,
|
|
213
|
-
longestStreakDays: 0,
|
|
214
|
-
longestStreakContact: null
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
let sent = 0;
|
|
218
|
-
let received = 0;
|
|
219
|
-
let reactions = 0;
|
|
220
|
-
const perContact = /* @__PURE__ */ new Map();
|
|
221
|
-
const perDay = /* @__PURE__ */ new Map();
|
|
222
|
-
for (const m of messages) {
|
|
223
|
-
if (m.isReaction) {
|
|
224
|
-
reactions++;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (m.isFromMe) sent++;
|
|
228
|
-
else received++;
|
|
229
|
-
const c = contactKey(m);
|
|
230
|
-
if (!perContact.has(c)) perContact.set(c, { sent: 0, received: 0, total: 0 });
|
|
231
|
-
const pc = perContact.get(c);
|
|
232
|
-
if (m.isFromMe) pc.sent++;
|
|
233
|
-
else pc.received++;
|
|
234
|
-
pc.total++;
|
|
235
|
-
const dk = localDateKey(m.date);
|
|
236
|
-
perDay.set(dk, (perDay.get(dk) ?? 0) + 1);
|
|
237
|
-
}
|
|
238
|
-
const topContacts = Array.from(perContact.entries()).map(([contact, v]) => ({ contact, ...v })).sort((a, b) => b.total - a.total).slice(0, 5);
|
|
239
|
-
let peakDay = null;
|
|
240
|
-
for (const [d, count] of perDay) {
|
|
241
|
-
if (!peakDay || count > peakDay.count) peakDay = { date: d, count };
|
|
242
|
-
}
|
|
243
|
-
const streaks = computeStreaks(messages);
|
|
244
|
-
const top = streaks[0];
|
|
245
|
-
return {
|
|
246
|
-
windowStart: localDateKey(messages[0].date),
|
|
247
|
-
windowEnd: localDateKey(messages[messages.length - 1].date),
|
|
248
|
-
totalSent: sent,
|
|
249
|
-
totalReceived: received,
|
|
250
|
-
totalReactions: reactions,
|
|
251
|
-
topContacts,
|
|
252
|
-
peakDay,
|
|
253
|
-
longestStreakDays: top?.longestStreakDays ?? 0,
|
|
254
|
-
longestStreakContact: top?.contact ?? null
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
function dispatchAnalytic(type, messages) {
|
|
258
|
-
switch (type) {
|
|
259
|
-
case "messaging_streaks":
|
|
260
|
-
return { type, data: computeStreaks(messages) };
|
|
261
|
-
case "double_texts":
|
|
262
|
-
return { type, data: computeDoubleTexts(messages) };
|
|
263
|
-
case "response_time_stats":
|
|
264
|
-
return { type, data: computeResponseTimes(messages) };
|
|
265
|
-
case "daily_heatmap":
|
|
266
|
-
return { type, data: computeHeatmap(messages) };
|
|
267
|
-
case "tapback_summary":
|
|
268
|
-
return { type, data: computeTapbacks(messages) };
|
|
269
|
-
case "year_in_review_wrapped":
|
|
270
|
-
return { type, data: computeWrapped(messages) };
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
20
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
274
21
|
let dbPath = join(homedir(), ".imsg-mcp", "analytics-cache.db");
|
|
275
22
|
let db = null;
|
|
@@ -342,81 +89,6 @@ function resolveContactSelector(selector) {
|
|
|
342
89
|
}
|
|
343
90
|
return null;
|
|
344
91
|
}
|
|
345
|
-
const EMOJI_REGEX = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}]/gu;
|
|
346
|
-
const WHITESPACE_REGEX = /\s+/g;
|
|
347
|
-
function cleanText(s) {
|
|
348
|
-
return s.toLowerCase().replace(EMOJI_REGEX, " ").replace(WHITESPACE_REGEX, " ").trim();
|
|
349
|
-
}
|
|
350
|
-
function tokenSet(s) {
|
|
351
|
-
if (!s) return /* @__PURE__ */ new Set();
|
|
352
|
-
return new Set(s.split(" ").filter(Boolean));
|
|
353
|
-
}
|
|
354
|
-
function levenshtein(a, b, maxLen = 200) {
|
|
355
|
-
if (a === b) return 0;
|
|
356
|
-
const aLen = Math.min(a.length, maxLen);
|
|
357
|
-
const bLen = Math.min(b.length, maxLen);
|
|
358
|
-
if (aLen === 0) return bLen;
|
|
359
|
-
if (bLen === 0) return aLen;
|
|
360
|
-
let prev = new Array(bLen + 1);
|
|
361
|
-
let curr = new Array(bLen + 1);
|
|
362
|
-
for (let j = 0; j <= bLen; j++) prev[j] = j;
|
|
363
|
-
for (let i = 1; i <= aLen; i++) {
|
|
364
|
-
curr[0] = i;
|
|
365
|
-
for (let j = 1; j <= bLen; j++) {
|
|
366
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
367
|
-
const del = (prev[j] ?? 0) + 1;
|
|
368
|
-
const ins = (curr[j - 1] ?? 0) + 1;
|
|
369
|
-
const sub = (prev[j - 1] ?? 0) + cost;
|
|
370
|
-
curr[j] = Math.min(del, ins, sub);
|
|
371
|
-
}
|
|
372
|
-
[prev, curr] = [curr, prev];
|
|
373
|
-
}
|
|
374
|
-
return prev[bLen] ?? Math.max(aLen, bLen);
|
|
375
|
-
}
|
|
376
|
-
function levenshteinRatio(a, b) {
|
|
377
|
-
if (a === b) return 1;
|
|
378
|
-
const aLen = Math.min(a.length, 200);
|
|
379
|
-
const bLen = Math.min(b.length, 200);
|
|
380
|
-
const maxLen = Math.max(aLen, bLen);
|
|
381
|
-
if (maxLen === 0) return 1;
|
|
382
|
-
return 1 - levenshtein(a, b) / maxLen;
|
|
383
|
-
}
|
|
384
|
-
function fuzzyScore(query, candidate) {
|
|
385
|
-
if (query && candidate && candidate.toLowerCase().includes(query.toLowerCase())) {
|
|
386
|
-
return 0.95;
|
|
387
|
-
}
|
|
388
|
-
const q = cleanText(query);
|
|
389
|
-
const c = cleanText(candidate);
|
|
390
|
-
if (!q || !c) return 0;
|
|
391
|
-
if (c.includes(q)) return 0.95;
|
|
392
|
-
if (q.includes(c)) return 0.9;
|
|
393
|
-
const qTokens = tokenSet(q);
|
|
394
|
-
const cTokens = tokenSet(c);
|
|
395
|
-
let intersection = 0;
|
|
396
|
-
for (const tok of qTokens) {
|
|
397
|
-
if (cTokens.has(tok)) intersection++;
|
|
398
|
-
}
|
|
399
|
-
const dice = 2 * intersection / (qTokens.size + cTokens.size || 1);
|
|
400
|
-
let bestTokenLev = 0;
|
|
401
|
-
if (qTokens.size === 1) {
|
|
402
|
-
for (const tok of cTokens) {
|
|
403
|
-
const r = levenshteinRatio(q, tok);
|
|
404
|
-
if (r > bestTokenLev) bestTokenLev = r;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
const levRatio = Math.max(levenshteinRatio(q, c), bestTokenLev);
|
|
408
|
-
const blend = qTokens.size <= 1 ? 0.1 * dice + 0.9 * levRatio : 0.7 * dice + 0.3 * levRatio;
|
|
409
|
-
return Math.max(0, Math.min(1, blend));
|
|
410
|
-
}
|
|
411
|
-
function rankFuzzy(query, candidates, getText, minScore = 0.6) {
|
|
412
|
-
const out = [];
|
|
413
|
-
for (const c of candidates) {
|
|
414
|
-
const score = fuzzyScore(query, getText(c));
|
|
415
|
-
if (score >= minScore) out.push({ item: c, score });
|
|
416
|
-
}
|
|
417
|
-
out.sort((a, b) => b.score - a.score);
|
|
418
|
-
return out;
|
|
419
|
-
}
|
|
420
92
|
const ignoreOverride = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
421
93
|
const defaultOptions = {
|
|
422
94
|
name: void 0,
|