imsg-mcp 1.0.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 +6 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/cli.js +611 -0
- package/dist/cli.js.map +1 -0
- package/dist/dateParse-DJXMfq3a.js +74 -0
- package/dist/dateParse-DJXMfq3a.js.map +1 -0
- package/dist/exportFormats-CWWiy5uz.js +108 -0
- package/dist/exportFormats-CWWiy5uz.js.map +1 -0
- package/dist/exportStream-BaheQ6M4.js +130 -0
- package/dist/exportStream-BaheQ6M4.js.map +1 -0
- package/dist/imessage-db-BVDtx0Sn.js +2263 -0
- package/dist/imessage-db-BVDtx0Sn.js.map +1 -0
- package/dist/index.js +3503 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-D3NoTAjA.js +7 -0
- package/dist/meta-D3NoTAjA.js.map +1 -0
- package/dist/setup-DMckHRnI.js +103 -0
- package/dist/setup-DMckHRnI.js.map +1 -0
- package/dist/shutdown-B9ClCyco.js +775 -0
- package/dist/shutdown-B9ClCyco.js.map +1 -0
- package/dist/tui-config-Crn6TZPg.js +122 -0
- package/dist/tui-config-Crn6TZPg.js.map +1 -0
- package/dist/tui.js +2706 -0
- package/dist/tui.js.map +1 -0
- package/dist/watchdog-V3lgEhMp.js +207 -0
- package/dist/watchdog-V3lgEhMp.js.map +1 -0
- package/native/imsg-native.darwin-arm64.node +0 -0
- package/native/index.d.ts +86 -0
- package/native/index.js +582 -0
- package/package.json +135 -0
- package/skills/imsg-mcp/SKILL.md +168 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3503 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join, dirname, isAbsolute } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
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 logShutdown, A as stopHeapMonitor, B as getLogFilePath, C as getLastSendError } from "./shutdown-B9ClCyco.js";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import Database from "better-sqlite3";
|
|
12
|
+
import { streamExport } from "./exportStream-BaheQ6M4.js";
|
|
13
|
+
import { I as IMessageDB, h as hasNativeModule } from "./imessage-db-BVDtx0Sn.js";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { A as APP_VERSION, a as APP_NAME } from "./meta-D3NoTAjA.js";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { parseUserDate } from "./dateParse-DJXMfq3a.js";
|
|
18
|
+
import { i as installWatchdog, n as noteActivity, r as readWatchdogState } from "./watchdog-V3lgEhMp.js";
|
|
19
|
+
const MS_PER_DAY = 864e5;
|
|
20
|
+
function localDateKey(d) {
|
|
21
|
+
const y = d.getFullYear();
|
|
22
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
23
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
24
|
+
return `${y}-${m}-${day}`;
|
|
25
|
+
}
|
|
26
|
+
function contactKey(m) {
|
|
27
|
+
return m.chatId;
|
|
28
|
+
}
|
|
29
|
+
function byDate(a, b) {
|
|
30
|
+
return a.date.getTime() - b.date.getTime();
|
|
31
|
+
}
|
|
32
|
+
function computeStreaks(messages) {
|
|
33
|
+
const byContact = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const m of messages) {
|
|
35
|
+
if (m.isReaction) continue;
|
|
36
|
+
const c = contactKey(m);
|
|
37
|
+
if (!byContact.has(c)) byContact.set(c, /* @__PURE__ */ new Set());
|
|
38
|
+
byContact.get(c).add(localDateKey(m.date));
|
|
39
|
+
}
|
|
40
|
+
localDateKey(/* @__PURE__ */ new Date());
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const [contact, dayset] of byContact) {
|
|
43
|
+
const days = Array.from(dayset).sort();
|
|
44
|
+
let longest = 0;
|
|
45
|
+
let longestStart = null;
|
|
46
|
+
let longestEnd = null;
|
|
47
|
+
let runLen = 0;
|
|
48
|
+
let runStart = null;
|
|
49
|
+
for (let i = 0; i < days.length; i++) {
|
|
50
|
+
if (i === 0 || new Date(days[i]).getTime() - new Date(days[i - 1]).getTime() !== MS_PER_DAY) {
|
|
51
|
+
runLen = 1;
|
|
52
|
+
runStart = days[i];
|
|
53
|
+
} else {
|
|
54
|
+
runLen++;
|
|
55
|
+
}
|
|
56
|
+
if (runLen > longest) {
|
|
57
|
+
longest = runLen;
|
|
58
|
+
longestStart = runStart;
|
|
59
|
+
longestEnd = days[i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let curLen = 0;
|
|
63
|
+
const cursor = /* @__PURE__ */ new Date();
|
|
64
|
+
while (dayset.has(localDateKey(cursor))) {
|
|
65
|
+
curLen++;
|
|
66
|
+
cursor.setDate(cursor.getDate() - 1);
|
|
67
|
+
}
|
|
68
|
+
results.push({
|
|
69
|
+
contact,
|
|
70
|
+
longestStreakDays: longest,
|
|
71
|
+
longestStreakStart: longestStart,
|
|
72
|
+
longestStreakEnd: longestEnd,
|
|
73
|
+
currentStreakDays: curLen
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
results.sort((a, b) => b.longestStreakDays - a.longestStreakDays);
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
function computeDoubleTexts(messages) {
|
|
80
|
+
const byContact = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const m of messages) {
|
|
82
|
+
if (m.isReaction) continue;
|
|
83
|
+
const c = contactKey(m);
|
|
84
|
+
if (!byContact.has(c)) byContact.set(c, []);
|
|
85
|
+
byContact.get(c).push(m);
|
|
86
|
+
}
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const [contact, msgs] of byContact) {
|
|
89
|
+
msgs.sort(byDate);
|
|
90
|
+
let mineRun = 0;
|
|
91
|
+
let theirsRun = 0;
|
|
92
|
+
let mineDoubles = 0;
|
|
93
|
+
let theirsDoubles = 0;
|
|
94
|
+
for (const m of msgs) {
|
|
95
|
+
if (m.isFromMe) {
|
|
96
|
+
mineRun++;
|
|
97
|
+
theirsRun = 0;
|
|
98
|
+
if (mineRun >= 2) mineDoubles++;
|
|
99
|
+
} else {
|
|
100
|
+
theirsRun++;
|
|
101
|
+
mineRun = 0;
|
|
102
|
+
if (theirsRun >= 2) theirsDoubles++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
results.push({
|
|
106
|
+
contact,
|
|
107
|
+
doubleTextsFromMe: mineDoubles,
|
|
108
|
+
doubleTextsFromThem: theirsDoubles
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
results.sort(
|
|
112
|
+
(a, b) => b.doubleTextsFromMe + b.doubleTextsFromThem - (a.doubleTextsFromMe + a.doubleTextsFromThem)
|
|
113
|
+
);
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
function percentile(sorted, p) {
|
|
117
|
+
if (sorted.length === 0) return 0;
|
|
118
|
+
const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
|
|
119
|
+
return sorted[idx] ?? 0;
|
|
120
|
+
}
|
|
121
|
+
function computeResponseTimes(messages) {
|
|
122
|
+
const byContact = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const m of messages) {
|
|
124
|
+
if (m.isReaction) continue;
|
|
125
|
+
const c = contactKey(m);
|
|
126
|
+
if (!byContact.has(c)) byContact.set(c, []);
|
|
127
|
+
byContact.get(c).push(m);
|
|
128
|
+
}
|
|
129
|
+
const results = [];
|
|
130
|
+
for (const [contact, msgs] of byContact) {
|
|
131
|
+
msgs.sort(byDate);
|
|
132
|
+
const deltas = [];
|
|
133
|
+
for (let i = 1; i < msgs.length; i++) {
|
|
134
|
+
const prev = msgs[i - 1];
|
|
135
|
+
const curr = msgs[i];
|
|
136
|
+
if (!prev.isFromMe && curr.isFromMe) {
|
|
137
|
+
deltas.push(curr.date.getTime() - prev.date.getTime());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (deltas.length === 0) continue;
|
|
141
|
+
deltas.sort((a, b) => a - b);
|
|
142
|
+
const mean = deltas.reduce((s, n) => s + n, 0) / deltas.length;
|
|
143
|
+
results.push({
|
|
144
|
+
contact,
|
|
145
|
+
count: deltas.length,
|
|
146
|
+
medianMs: percentile(deltas, 50),
|
|
147
|
+
p95Ms: percentile(deltas, 95),
|
|
148
|
+
meanMs: Math.round(mean)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
results.sort((a, b) => a.medianMs - b.medianMs);
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
function computeHeatmap(messages) {
|
|
155
|
+
const grid = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));
|
|
156
|
+
let total = 0;
|
|
157
|
+
for (const m of messages) {
|
|
158
|
+
if (m.isReaction) continue;
|
|
159
|
+
const dow = m.date.getDay();
|
|
160
|
+
const hr = m.date.getHours();
|
|
161
|
+
const dowRow = grid[dow];
|
|
162
|
+
if (dowRow && hr >= 0 && hr < 24) {
|
|
163
|
+
dowRow[hr] = (dowRow[hr] ?? 0) + 1;
|
|
164
|
+
total++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { grid, total };
|
|
168
|
+
}
|
|
169
|
+
function computeTapbacks(messages) {
|
|
170
|
+
const byContact = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const m of messages) {
|
|
172
|
+
if (!m.isReaction || !m.reaction) continue;
|
|
173
|
+
const c = contactKey(m);
|
|
174
|
+
if (!byContact.has(c)) {
|
|
175
|
+
byContact.set(c, {
|
|
176
|
+
contact: c,
|
|
177
|
+
heart: 0,
|
|
178
|
+
thumbsUp: 0,
|
|
179
|
+
thumbsDown: 0,
|
|
180
|
+
haha: 0,
|
|
181
|
+
exclaim: 0,
|
|
182
|
+
question: 0,
|
|
183
|
+
emoji: 0,
|
|
184
|
+
total: 0
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const r = byContact.get(c);
|
|
188
|
+
const t = (m.reaction.type || "").toLowerCase();
|
|
189
|
+
if (t.includes("heart") || t === "love") r.heart++;
|
|
190
|
+
else if (t.includes("thumb") && t.includes("up") || t === "like") r.thumbsUp++;
|
|
191
|
+
else if (t.includes("thumb") && t.includes("down") || t === "dislike") r.thumbsDown++;
|
|
192
|
+
else if (t.includes("haha") || t === "laugh") r.haha++;
|
|
193
|
+
else if (t.includes("exclaim") || t === "emphasis") r.exclaim++;
|
|
194
|
+
else if (t.includes("question")) r.question++;
|
|
195
|
+
else r.emoji++;
|
|
196
|
+
r.total++;
|
|
197
|
+
}
|
|
198
|
+
const out = Array.from(byContact.values());
|
|
199
|
+
out.sort((a, b) => b.total - a.total);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
function computeWrapped(messages) {
|
|
203
|
+
if (messages.length === 0) {
|
|
204
|
+
return {
|
|
205
|
+
windowStart: "",
|
|
206
|
+
windowEnd: "",
|
|
207
|
+
totalSent: 0,
|
|
208
|
+
totalReceived: 0,
|
|
209
|
+
totalReactions: 0,
|
|
210
|
+
topContacts: [],
|
|
211
|
+
peakDay: null,
|
|
212
|
+
longestStreakDays: 0,
|
|
213
|
+
longestStreakContact: null
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
let sent = 0;
|
|
217
|
+
let received = 0;
|
|
218
|
+
let reactions = 0;
|
|
219
|
+
const perContact = /* @__PURE__ */ new Map();
|
|
220
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const m of messages) {
|
|
222
|
+
if (m.isReaction) {
|
|
223
|
+
reactions++;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (m.isFromMe) sent++;
|
|
227
|
+
else received++;
|
|
228
|
+
const c = contactKey(m);
|
|
229
|
+
if (!perContact.has(c)) perContact.set(c, { sent: 0, received: 0, total: 0 });
|
|
230
|
+
const pc = perContact.get(c);
|
|
231
|
+
if (m.isFromMe) pc.sent++;
|
|
232
|
+
else pc.received++;
|
|
233
|
+
pc.total++;
|
|
234
|
+
const dk = localDateKey(m.date);
|
|
235
|
+
perDay.set(dk, (perDay.get(dk) ?? 0) + 1);
|
|
236
|
+
}
|
|
237
|
+
const topContacts = Array.from(perContact.entries()).map(([contact, v]) => ({ contact, ...v })).sort((a, b) => b.total - a.total).slice(0, 5);
|
|
238
|
+
let peakDay = null;
|
|
239
|
+
for (const [d, count] of perDay) {
|
|
240
|
+
if (!peakDay || count > peakDay.count) peakDay = { date: d, count };
|
|
241
|
+
}
|
|
242
|
+
const streaks = computeStreaks(messages);
|
|
243
|
+
const top = streaks[0];
|
|
244
|
+
return {
|
|
245
|
+
windowStart: localDateKey(messages[0].date),
|
|
246
|
+
windowEnd: localDateKey(messages[messages.length - 1].date),
|
|
247
|
+
totalSent: sent,
|
|
248
|
+
totalReceived: received,
|
|
249
|
+
totalReactions: reactions,
|
|
250
|
+
topContacts,
|
|
251
|
+
peakDay,
|
|
252
|
+
longestStreakDays: top?.longestStreakDays ?? 0,
|
|
253
|
+
longestStreakContact: top?.contact ?? null
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function dispatchAnalytic(type, messages) {
|
|
257
|
+
switch (type) {
|
|
258
|
+
case "messaging_streaks":
|
|
259
|
+
return { type, data: computeStreaks(messages) };
|
|
260
|
+
case "double_texts":
|
|
261
|
+
return { type, data: computeDoubleTexts(messages) };
|
|
262
|
+
case "response_time_stats":
|
|
263
|
+
return { type, data: computeResponseTimes(messages) };
|
|
264
|
+
case "daily_heatmap":
|
|
265
|
+
return { type, data: computeHeatmap(messages) };
|
|
266
|
+
case "tapback_summary":
|
|
267
|
+
return { type, data: computeTapbacks(messages) };
|
|
268
|
+
case "year_in_review_wrapped":
|
|
269
|
+
return { type, data: computeWrapped(messages) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
273
|
+
let dbPath = join(homedir(), ".imsg-mcp", "analytics-cache.db");
|
|
274
|
+
let db = null;
|
|
275
|
+
function open() {
|
|
276
|
+
if (db) return db;
|
|
277
|
+
const dir = dirname(dbPath);
|
|
278
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
279
|
+
db = new Database(dbPath);
|
|
280
|
+
db.pragma("journal_mode = WAL");
|
|
281
|
+
db.exec(`
|
|
282
|
+
CREATE TABLE IF NOT EXISTS analytics_cache (
|
|
283
|
+
type TEXT NOT NULL,
|
|
284
|
+
args_hash TEXT NOT NULL,
|
|
285
|
+
max_rowid INTEGER NOT NULL,
|
|
286
|
+
data_json TEXT NOT NULL,
|
|
287
|
+
computed_at INTEGER NOT NULL,
|
|
288
|
+
PRIMARY KEY (type, args_hash)
|
|
289
|
+
);
|
|
290
|
+
`);
|
|
291
|
+
return db;
|
|
292
|
+
}
|
|
293
|
+
function hashArgs(args) {
|
|
294
|
+
return JSON.stringify(args, Object.keys(args).sort());
|
|
295
|
+
}
|
|
296
|
+
function lookupCache(type, args, maxRowId) {
|
|
297
|
+
const conn = open();
|
|
298
|
+
const row = conn.prepare(
|
|
299
|
+
`SELECT data_json, max_rowid, computed_at FROM analytics_cache
|
|
300
|
+
WHERE type = ? AND args_hash = ?`
|
|
301
|
+
).get(type, hashArgs(args));
|
|
302
|
+
if (!row) return null;
|
|
303
|
+
if (row.max_rowid !== maxRowId) return null;
|
|
304
|
+
if (Date.now() - row.computed_at > DEFAULT_TTL_MS) return null;
|
|
305
|
+
return { data: JSON.parse(row.data_json), computedAt: row.computed_at };
|
|
306
|
+
}
|
|
307
|
+
function storeCache(type, args, maxRowId, data) {
|
|
308
|
+
const conn = open();
|
|
309
|
+
conn.prepare(
|
|
310
|
+
`INSERT INTO analytics_cache (type, args_hash, max_rowid, data_json, computed_at)
|
|
311
|
+
VALUES (?, ?, ?, ?, ?)
|
|
312
|
+
ON CONFLICT(type, args_hash) DO UPDATE SET
|
|
313
|
+
max_rowid = excluded.max_rowid,
|
|
314
|
+
data_json = excluded.data_json,
|
|
315
|
+
computed_at = excluded.computed_at`
|
|
316
|
+
).run(type, hashArgs(args), maxRowId, JSON.stringify(data), Date.now());
|
|
317
|
+
}
|
|
318
|
+
const MAX_ENTRIES = 10;
|
|
319
|
+
const lru = [];
|
|
320
|
+
function rememberSearch(query, matches) {
|
|
321
|
+
if (matches.length === 0) return;
|
|
322
|
+
const dupIdx = lru.findIndex((e) => e.query === query);
|
|
323
|
+
if (dupIdx >= 0) lru.splice(dupIdx, 1);
|
|
324
|
+
lru.unshift({ query, recordedAt: Date.now(), matches });
|
|
325
|
+
if (lru.length > MAX_ENTRIES) lru.length = MAX_ENTRIES;
|
|
326
|
+
}
|
|
327
|
+
function resolveContactSelector(selector) {
|
|
328
|
+
const m = selector.match(/^contact:(\d+)$/i);
|
|
329
|
+
if (!m?.[1]) return null;
|
|
330
|
+
const idx = Number.parseInt(m[1], 10) - 1;
|
|
331
|
+
if (idx < 0) return null;
|
|
332
|
+
for (const entry of lru) {
|
|
333
|
+
if (idx < entry.matches.length) {
|
|
334
|
+
const hit = entry.matches[idx];
|
|
335
|
+
if (hit) return hit;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const EMOJI_REGEX = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F1E6}-\u{1F1FF}]/gu;
|
|
341
|
+
const WHITESPACE_REGEX = /\s+/g;
|
|
342
|
+
function cleanText(s) {
|
|
343
|
+
return s.toLowerCase().replace(EMOJI_REGEX, " ").replace(WHITESPACE_REGEX, " ").trim();
|
|
344
|
+
}
|
|
345
|
+
function tokenSet(s) {
|
|
346
|
+
if (!s) return /* @__PURE__ */ new Set();
|
|
347
|
+
return new Set(s.split(" ").filter(Boolean));
|
|
348
|
+
}
|
|
349
|
+
function levenshtein(a, b, maxLen = 200) {
|
|
350
|
+
if (a === b) return 0;
|
|
351
|
+
const aLen = Math.min(a.length, maxLen);
|
|
352
|
+
const bLen = Math.min(b.length, maxLen);
|
|
353
|
+
if (aLen === 0) return bLen;
|
|
354
|
+
if (bLen === 0) return aLen;
|
|
355
|
+
let prev = new Array(bLen + 1);
|
|
356
|
+
let curr = new Array(bLen + 1);
|
|
357
|
+
for (let j = 0; j <= bLen; j++) prev[j] = j;
|
|
358
|
+
for (let i = 1; i <= aLen; i++) {
|
|
359
|
+
curr[0] = i;
|
|
360
|
+
for (let j = 1; j <= bLen; j++) {
|
|
361
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
362
|
+
const del = (prev[j] ?? 0) + 1;
|
|
363
|
+
const ins = (curr[j - 1] ?? 0) + 1;
|
|
364
|
+
const sub = (prev[j - 1] ?? 0) + cost;
|
|
365
|
+
curr[j] = Math.min(del, ins, sub);
|
|
366
|
+
}
|
|
367
|
+
[prev, curr] = [curr, prev];
|
|
368
|
+
}
|
|
369
|
+
return prev[bLen] ?? Math.max(aLen, bLen);
|
|
370
|
+
}
|
|
371
|
+
function levenshteinRatio(a, b) {
|
|
372
|
+
if (a === b) return 1;
|
|
373
|
+
const aLen = Math.min(a.length, 200);
|
|
374
|
+
const bLen = Math.min(b.length, 200);
|
|
375
|
+
const maxLen = Math.max(aLen, bLen);
|
|
376
|
+
if (maxLen === 0) return 1;
|
|
377
|
+
return 1 - levenshtein(a, b) / maxLen;
|
|
378
|
+
}
|
|
379
|
+
function fuzzyScore(query, candidate) {
|
|
380
|
+
const q = cleanText(query);
|
|
381
|
+
const c = cleanText(candidate);
|
|
382
|
+
if (!q || !c) return 0;
|
|
383
|
+
if (c.includes(q)) return 0.95;
|
|
384
|
+
if (q.includes(c)) return 0.9;
|
|
385
|
+
const qTokens = tokenSet(q);
|
|
386
|
+
const cTokens = tokenSet(c);
|
|
387
|
+
let intersection = 0;
|
|
388
|
+
for (const tok of qTokens) {
|
|
389
|
+
if (cTokens.has(tok)) intersection++;
|
|
390
|
+
}
|
|
391
|
+
const dice = 2 * intersection / (qTokens.size + cTokens.size || 1);
|
|
392
|
+
let bestTokenLev = 0;
|
|
393
|
+
if (qTokens.size === 1) {
|
|
394
|
+
for (const tok of cTokens) {
|
|
395
|
+
const r = levenshteinRatio(q, tok);
|
|
396
|
+
if (r > bestTokenLev) bestTokenLev = r;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const levRatio = Math.max(levenshteinRatio(q, c), bestTokenLev);
|
|
400
|
+
const blend = qTokens.size <= 1 ? 0.1 * dice + 0.9 * levRatio : 0.7 * dice + 0.3 * levRatio;
|
|
401
|
+
return Math.max(0, Math.min(1, blend));
|
|
402
|
+
}
|
|
403
|
+
function rankFuzzy(query, candidates, getText, minScore = 0.6) {
|
|
404
|
+
const out = [];
|
|
405
|
+
for (const c of candidates) {
|
|
406
|
+
const score = fuzzyScore(query, getText(c));
|
|
407
|
+
if (score >= minScore) out.push({ item: c, score });
|
|
408
|
+
}
|
|
409
|
+
out.sort((a, b) => b.score - a.score);
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
const ignoreOverride = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
413
|
+
const defaultOptions = {
|
|
414
|
+
name: void 0,
|
|
415
|
+
$refStrategy: "root",
|
|
416
|
+
basePath: ["#"],
|
|
417
|
+
effectStrategy: "input",
|
|
418
|
+
pipeStrategy: "all",
|
|
419
|
+
dateStrategy: "format:date-time",
|
|
420
|
+
mapStrategy: "entries",
|
|
421
|
+
removeAdditionalStrategy: "passthrough",
|
|
422
|
+
allowedAdditionalProperties: true,
|
|
423
|
+
rejectedAdditionalProperties: false,
|
|
424
|
+
definitionPath: "definitions",
|
|
425
|
+
target: "jsonSchema7",
|
|
426
|
+
strictUnions: false,
|
|
427
|
+
definitions: {},
|
|
428
|
+
errorMessages: false,
|
|
429
|
+
markdownDescription: false,
|
|
430
|
+
patternStrategy: "escape",
|
|
431
|
+
applyRegexFlags: false,
|
|
432
|
+
emailStrategy: "format:email",
|
|
433
|
+
base64Strategy: "contentEncoding:base64",
|
|
434
|
+
nameStrategy: "ref",
|
|
435
|
+
openAiAnyTypeName: "OpenAiAnyType"
|
|
436
|
+
};
|
|
437
|
+
const getDefaultOptions = (options) => ({
|
|
438
|
+
...defaultOptions,
|
|
439
|
+
...options
|
|
440
|
+
});
|
|
441
|
+
const getRefs = (options) => {
|
|
442
|
+
const _options = getDefaultOptions(options);
|
|
443
|
+
const currentPath = _options.name !== void 0 ? [..._options.basePath, _options.definitionPath, _options.name] : _options.basePath;
|
|
444
|
+
return {
|
|
445
|
+
..._options,
|
|
446
|
+
flags: { hasReferencedOpenAiAnyType: false },
|
|
447
|
+
currentPath,
|
|
448
|
+
propertyPath: void 0,
|
|
449
|
+
seen: new Map(Object.entries(_options.definitions).map(([name, def]) => [
|
|
450
|
+
def._def,
|
|
451
|
+
{
|
|
452
|
+
def: def._def,
|
|
453
|
+
path: [..._options.basePath, _options.definitionPath, name],
|
|
454
|
+
// Resolution of references will be forced even though seen, so it's ok that the schema is undefined here for now.
|
|
455
|
+
jsonSchema: void 0
|
|
456
|
+
}
|
|
457
|
+
]))
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
function addErrorMessage(res, key, errorMessage, refs) {
|
|
461
|
+
if (!refs?.errorMessages)
|
|
462
|
+
return;
|
|
463
|
+
if (errorMessage) {
|
|
464
|
+
res.errorMessage = {
|
|
465
|
+
...res.errorMessage,
|
|
466
|
+
[key]: errorMessage
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
|
|
471
|
+
res[key] = value;
|
|
472
|
+
addErrorMessage(res, key, errorMessage, refs);
|
|
473
|
+
}
|
|
474
|
+
const getRelativePath = (pathA, pathB) => {
|
|
475
|
+
let i = 0;
|
|
476
|
+
for (; i < pathA.length && i < pathB.length; i++) {
|
|
477
|
+
if (pathA[i] !== pathB[i])
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
return [(pathA.length - i).toString(), ...pathB.slice(i)].join("/");
|
|
481
|
+
};
|
|
482
|
+
var ZodFirstPartyTypeKind;
|
|
483
|
+
(function(ZodFirstPartyTypeKind2) {
|
|
484
|
+
ZodFirstPartyTypeKind2["ZodString"] = "ZodString";
|
|
485
|
+
ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber";
|
|
486
|
+
ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN";
|
|
487
|
+
ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt";
|
|
488
|
+
ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean";
|
|
489
|
+
ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate";
|
|
490
|
+
ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol";
|
|
491
|
+
ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined";
|
|
492
|
+
ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull";
|
|
493
|
+
ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny";
|
|
494
|
+
ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown";
|
|
495
|
+
ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever";
|
|
496
|
+
ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid";
|
|
497
|
+
ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray";
|
|
498
|
+
ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject";
|
|
499
|
+
ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion";
|
|
500
|
+
ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion";
|
|
501
|
+
ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection";
|
|
502
|
+
ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple";
|
|
503
|
+
ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord";
|
|
504
|
+
ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap";
|
|
505
|
+
ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet";
|
|
506
|
+
ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction";
|
|
507
|
+
ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy";
|
|
508
|
+
ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral";
|
|
509
|
+
ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum";
|
|
510
|
+
ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects";
|
|
511
|
+
ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum";
|
|
512
|
+
ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional";
|
|
513
|
+
ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable";
|
|
514
|
+
ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault";
|
|
515
|
+
ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch";
|
|
516
|
+
ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise";
|
|
517
|
+
ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded";
|
|
518
|
+
ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline";
|
|
519
|
+
ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly";
|
|
520
|
+
})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
|
|
521
|
+
function parseAnyDef(refs) {
|
|
522
|
+
if (refs.target !== "openAi") {
|
|
523
|
+
return {};
|
|
524
|
+
}
|
|
525
|
+
const anyDefinitionPath = [
|
|
526
|
+
...refs.basePath,
|
|
527
|
+
refs.definitionPath,
|
|
528
|
+
refs.openAiAnyTypeName
|
|
529
|
+
];
|
|
530
|
+
refs.flags.hasReferencedOpenAiAnyType = true;
|
|
531
|
+
return {
|
|
532
|
+
$ref: refs.$refStrategy === "relative" ? getRelativePath(anyDefinitionPath, refs.currentPath) : anyDefinitionPath.join("/")
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function parseArrayDef(def, refs) {
|
|
536
|
+
const res = {
|
|
537
|
+
type: "array"
|
|
538
|
+
};
|
|
539
|
+
if (def.type?._def && def.type?._def?.typeName !== ZodFirstPartyTypeKind.ZodAny) {
|
|
540
|
+
res.items = parseDef(def.type._def, {
|
|
541
|
+
...refs,
|
|
542
|
+
currentPath: [...refs.currentPath, "items"]
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (def.minLength) {
|
|
546
|
+
setResponseValueAndErrors(res, "minItems", def.minLength.value, def.minLength.message, refs);
|
|
547
|
+
}
|
|
548
|
+
if (def.maxLength) {
|
|
549
|
+
setResponseValueAndErrors(res, "maxItems", def.maxLength.value, def.maxLength.message, refs);
|
|
550
|
+
}
|
|
551
|
+
if (def.exactLength) {
|
|
552
|
+
setResponseValueAndErrors(res, "minItems", def.exactLength.value, def.exactLength.message, refs);
|
|
553
|
+
setResponseValueAndErrors(res, "maxItems", def.exactLength.value, def.exactLength.message, refs);
|
|
554
|
+
}
|
|
555
|
+
return res;
|
|
556
|
+
}
|
|
557
|
+
function parseBigintDef(def, refs) {
|
|
558
|
+
const res = {
|
|
559
|
+
type: "integer",
|
|
560
|
+
format: "int64"
|
|
561
|
+
};
|
|
562
|
+
if (!def.checks)
|
|
563
|
+
return res;
|
|
564
|
+
for (const check of def.checks) {
|
|
565
|
+
switch (check.kind) {
|
|
566
|
+
case "min":
|
|
567
|
+
if (refs.target === "jsonSchema7") {
|
|
568
|
+
if (check.inclusive) {
|
|
569
|
+
setResponseValueAndErrors(res, "minimum", check.value, check.message, refs);
|
|
570
|
+
} else {
|
|
571
|
+
setResponseValueAndErrors(res, "exclusiveMinimum", check.value, check.message, refs);
|
|
572
|
+
}
|
|
573
|
+
} else {
|
|
574
|
+
if (!check.inclusive) {
|
|
575
|
+
res.exclusiveMinimum = true;
|
|
576
|
+
}
|
|
577
|
+
setResponseValueAndErrors(res, "minimum", check.value, check.message, refs);
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
case "max":
|
|
581
|
+
if (refs.target === "jsonSchema7") {
|
|
582
|
+
if (check.inclusive) {
|
|
583
|
+
setResponseValueAndErrors(res, "maximum", check.value, check.message, refs);
|
|
584
|
+
} else {
|
|
585
|
+
setResponseValueAndErrors(res, "exclusiveMaximum", check.value, check.message, refs);
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
if (!check.inclusive) {
|
|
589
|
+
res.exclusiveMaximum = true;
|
|
590
|
+
}
|
|
591
|
+
setResponseValueAndErrors(res, "maximum", check.value, check.message, refs);
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
case "multipleOf":
|
|
595
|
+
setResponseValueAndErrors(res, "multipleOf", check.value, check.message, refs);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return res;
|
|
600
|
+
}
|
|
601
|
+
function parseBooleanDef() {
|
|
602
|
+
return {
|
|
603
|
+
type: "boolean"
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function parseBrandedDef(_def, refs) {
|
|
607
|
+
return parseDef(_def.type._def, refs);
|
|
608
|
+
}
|
|
609
|
+
const parseCatchDef = (def, refs) => {
|
|
610
|
+
return parseDef(def.innerType._def, refs);
|
|
611
|
+
};
|
|
612
|
+
function parseDateDef(def, refs, overrideDateStrategy) {
|
|
613
|
+
const strategy = overrideDateStrategy ?? refs.dateStrategy;
|
|
614
|
+
if (Array.isArray(strategy)) {
|
|
615
|
+
return {
|
|
616
|
+
anyOf: strategy.map((item, i) => parseDateDef(def, refs, item))
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
switch (strategy) {
|
|
620
|
+
case "string":
|
|
621
|
+
case "format:date-time":
|
|
622
|
+
return {
|
|
623
|
+
type: "string",
|
|
624
|
+
format: "date-time"
|
|
625
|
+
};
|
|
626
|
+
case "format:date":
|
|
627
|
+
return {
|
|
628
|
+
type: "string",
|
|
629
|
+
format: "date"
|
|
630
|
+
};
|
|
631
|
+
case "integer":
|
|
632
|
+
return integerDateParser(def, refs);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const integerDateParser = (def, refs) => {
|
|
636
|
+
const res = {
|
|
637
|
+
type: "integer",
|
|
638
|
+
format: "unix-time"
|
|
639
|
+
};
|
|
640
|
+
if (refs.target === "openApi3") {
|
|
641
|
+
return res;
|
|
642
|
+
}
|
|
643
|
+
for (const check of def.checks) {
|
|
644
|
+
switch (check.kind) {
|
|
645
|
+
case "min":
|
|
646
|
+
setResponseValueAndErrors(
|
|
647
|
+
res,
|
|
648
|
+
"minimum",
|
|
649
|
+
check.value,
|
|
650
|
+
// This is in milliseconds
|
|
651
|
+
check.message,
|
|
652
|
+
refs
|
|
653
|
+
);
|
|
654
|
+
break;
|
|
655
|
+
case "max":
|
|
656
|
+
setResponseValueAndErrors(
|
|
657
|
+
res,
|
|
658
|
+
"maximum",
|
|
659
|
+
check.value,
|
|
660
|
+
// This is in milliseconds
|
|
661
|
+
check.message,
|
|
662
|
+
refs
|
|
663
|
+
);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return res;
|
|
668
|
+
};
|
|
669
|
+
function parseDefaultDef(_def, refs) {
|
|
670
|
+
return {
|
|
671
|
+
...parseDef(_def.innerType._def, refs),
|
|
672
|
+
default: _def.defaultValue()
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function parseEffectsDef(_def, refs) {
|
|
676
|
+
return refs.effectStrategy === "input" ? parseDef(_def.schema._def, refs) : parseAnyDef(refs);
|
|
677
|
+
}
|
|
678
|
+
function parseEnumDef(def) {
|
|
679
|
+
return {
|
|
680
|
+
type: "string",
|
|
681
|
+
enum: Array.from(def.values)
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const isJsonSchema7AllOfType = (type) => {
|
|
685
|
+
if ("type" in type && type.type === "string")
|
|
686
|
+
return false;
|
|
687
|
+
return "allOf" in type;
|
|
688
|
+
};
|
|
689
|
+
function parseIntersectionDef(def, refs) {
|
|
690
|
+
const allOf = [
|
|
691
|
+
parseDef(def.left._def, {
|
|
692
|
+
...refs,
|
|
693
|
+
currentPath: [...refs.currentPath, "allOf", "0"]
|
|
694
|
+
}),
|
|
695
|
+
parseDef(def.right._def, {
|
|
696
|
+
...refs,
|
|
697
|
+
currentPath: [...refs.currentPath, "allOf", "1"]
|
|
698
|
+
})
|
|
699
|
+
].filter((x) => !!x);
|
|
700
|
+
let unevaluatedProperties = refs.target === "jsonSchema2019-09" ? { unevaluatedProperties: false } : void 0;
|
|
701
|
+
const mergedAllOf = [];
|
|
702
|
+
allOf.forEach((schema) => {
|
|
703
|
+
if (isJsonSchema7AllOfType(schema)) {
|
|
704
|
+
mergedAllOf.push(...schema.allOf);
|
|
705
|
+
if (schema.unevaluatedProperties === void 0) {
|
|
706
|
+
unevaluatedProperties = void 0;
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
let nestedSchema = schema;
|
|
710
|
+
if ("additionalProperties" in schema && schema.additionalProperties === false) {
|
|
711
|
+
const { additionalProperties, ...rest } = schema;
|
|
712
|
+
nestedSchema = rest;
|
|
713
|
+
} else {
|
|
714
|
+
unevaluatedProperties = void 0;
|
|
715
|
+
}
|
|
716
|
+
mergedAllOf.push(nestedSchema);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
return mergedAllOf.length ? {
|
|
720
|
+
allOf: mergedAllOf,
|
|
721
|
+
...unevaluatedProperties
|
|
722
|
+
} : void 0;
|
|
723
|
+
}
|
|
724
|
+
function parseLiteralDef(def, refs) {
|
|
725
|
+
const parsedType = typeof def.value;
|
|
726
|
+
if (parsedType !== "bigint" && parsedType !== "number" && parsedType !== "boolean" && parsedType !== "string") {
|
|
727
|
+
return {
|
|
728
|
+
type: Array.isArray(def.value) ? "array" : "object"
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
if (refs.target === "openApi3") {
|
|
732
|
+
return {
|
|
733
|
+
type: parsedType === "bigint" ? "integer" : parsedType,
|
|
734
|
+
enum: [def.value]
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
type: parsedType === "bigint" ? "integer" : parsedType,
|
|
739
|
+
const: def.value
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
let emojiRegex = void 0;
|
|
743
|
+
const zodPatterns = {
|
|
744
|
+
/**
|
|
745
|
+
* `c` was changed to `[cC]` to replicate /i flag
|
|
746
|
+
*/
|
|
747
|
+
cuid: /^[cC][^\s-]{8,}$/,
|
|
748
|
+
cuid2: /^[0-9a-z]+$/,
|
|
749
|
+
ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
|
|
750
|
+
/**
|
|
751
|
+
* `a-z` was added to replicate /i flag
|
|
752
|
+
*/
|
|
753
|
+
email: /^(?!\.)(?!.*\.\.)([a-zA-Z0-9_'+\-\.]*)[a-zA-Z0-9_+-]@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/,
|
|
754
|
+
/**
|
|
755
|
+
* Constructed a valid Unicode RegExp
|
|
756
|
+
*
|
|
757
|
+
* Lazily instantiate since this type of regex isn't supported
|
|
758
|
+
* in all envs (e.g. React Native).
|
|
759
|
+
*
|
|
760
|
+
* See:
|
|
761
|
+
* https://github.com/colinhacks/zod/issues/2433
|
|
762
|
+
* Fix in Zod:
|
|
763
|
+
* https://github.com/colinhacks/zod/commit/9340fd51e48576a75adc919bff65dbc4a5d4c99b
|
|
764
|
+
*/
|
|
765
|
+
emoji: () => {
|
|
766
|
+
if (emojiRegex === void 0) {
|
|
767
|
+
emojiRegex = RegExp("^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$", "u");
|
|
768
|
+
}
|
|
769
|
+
return emojiRegex;
|
|
770
|
+
},
|
|
771
|
+
/**
|
|
772
|
+
* Unused
|
|
773
|
+
*/
|
|
774
|
+
uuid: /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
|
775
|
+
/**
|
|
776
|
+
* Unused
|
|
777
|
+
*/
|
|
778
|
+
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/,
|
|
779
|
+
ipv4Cidr: /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/,
|
|
780
|
+
/**
|
|
781
|
+
* Unused
|
|
782
|
+
*/
|
|
783
|
+
ipv6: /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/,
|
|
784
|
+
ipv6Cidr: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/,
|
|
785
|
+
base64: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/,
|
|
786
|
+
base64url: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/,
|
|
787
|
+
nanoid: /^[a-zA-Z0-9_-]{21}$/,
|
|
788
|
+
jwt: /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/
|
|
789
|
+
};
|
|
790
|
+
function parseStringDef(def, refs) {
|
|
791
|
+
const res = {
|
|
792
|
+
type: "string"
|
|
793
|
+
};
|
|
794
|
+
if (def.checks) {
|
|
795
|
+
for (const check of def.checks) {
|
|
796
|
+
switch (check.kind) {
|
|
797
|
+
case "min":
|
|
798
|
+
setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check.value) : check.value, check.message, refs);
|
|
799
|
+
break;
|
|
800
|
+
case "max":
|
|
801
|
+
setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check.value) : check.value, check.message, refs);
|
|
802
|
+
break;
|
|
803
|
+
case "email":
|
|
804
|
+
switch (refs.emailStrategy) {
|
|
805
|
+
case "format:email":
|
|
806
|
+
addFormat(res, "email", check.message, refs);
|
|
807
|
+
break;
|
|
808
|
+
case "format:idn-email":
|
|
809
|
+
addFormat(res, "idn-email", check.message, refs);
|
|
810
|
+
break;
|
|
811
|
+
case "pattern:zod":
|
|
812
|
+
addPattern(res, zodPatterns.email, check.message, refs);
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
break;
|
|
816
|
+
case "url":
|
|
817
|
+
addFormat(res, "uri", check.message, refs);
|
|
818
|
+
break;
|
|
819
|
+
case "uuid":
|
|
820
|
+
addFormat(res, "uuid", check.message, refs);
|
|
821
|
+
break;
|
|
822
|
+
case "regex":
|
|
823
|
+
addPattern(res, check.regex, check.message, refs);
|
|
824
|
+
break;
|
|
825
|
+
case "cuid":
|
|
826
|
+
addPattern(res, zodPatterns.cuid, check.message, refs);
|
|
827
|
+
break;
|
|
828
|
+
case "cuid2":
|
|
829
|
+
addPattern(res, zodPatterns.cuid2, check.message, refs);
|
|
830
|
+
break;
|
|
831
|
+
case "startsWith":
|
|
832
|
+
addPattern(res, RegExp(`^${escapeLiteralCheckValue(check.value, refs)}`), check.message, refs);
|
|
833
|
+
break;
|
|
834
|
+
case "endsWith":
|
|
835
|
+
addPattern(res, RegExp(`${escapeLiteralCheckValue(check.value, refs)}$`), check.message, refs);
|
|
836
|
+
break;
|
|
837
|
+
case "datetime":
|
|
838
|
+
addFormat(res, "date-time", check.message, refs);
|
|
839
|
+
break;
|
|
840
|
+
case "date":
|
|
841
|
+
addFormat(res, "date", check.message, refs);
|
|
842
|
+
break;
|
|
843
|
+
case "time":
|
|
844
|
+
addFormat(res, "time", check.message, refs);
|
|
845
|
+
break;
|
|
846
|
+
case "duration":
|
|
847
|
+
addFormat(res, "duration", check.message, refs);
|
|
848
|
+
break;
|
|
849
|
+
case "length":
|
|
850
|
+
setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check.value) : check.value, check.message, refs);
|
|
851
|
+
setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check.value) : check.value, check.message, refs);
|
|
852
|
+
break;
|
|
853
|
+
case "includes": {
|
|
854
|
+
addPattern(res, RegExp(escapeLiteralCheckValue(check.value, refs)), check.message, refs);
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
case "ip": {
|
|
858
|
+
if (check.version !== "v6") {
|
|
859
|
+
addFormat(res, "ipv4", check.message, refs);
|
|
860
|
+
}
|
|
861
|
+
if (check.version !== "v4") {
|
|
862
|
+
addFormat(res, "ipv6", check.message, refs);
|
|
863
|
+
}
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
case "base64url":
|
|
867
|
+
addPattern(res, zodPatterns.base64url, check.message, refs);
|
|
868
|
+
break;
|
|
869
|
+
case "jwt":
|
|
870
|
+
addPattern(res, zodPatterns.jwt, check.message, refs);
|
|
871
|
+
break;
|
|
872
|
+
case "cidr": {
|
|
873
|
+
if (check.version !== "v6") {
|
|
874
|
+
addPattern(res, zodPatterns.ipv4Cidr, check.message, refs);
|
|
875
|
+
}
|
|
876
|
+
if (check.version !== "v4") {
|
|
877
|
+
addPattern(res, zodPatterns.ipv6Cidr, check.message, refs);
|
|
878
|
+
}
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "emoji":
|
|
882
|
+
addPattern(res, zodPatterns.emoji(), check.message, refs);
|
|
883
|
+
break;
|
|
884
|
+
case "ulid": {
|
|
885
|
+
addPattern(res, zodPatterns.ulid, check.message, refs);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
case "base64": {
|
|
889
|
+
switch (refs.base64Strategy) {
|
|
890
|
+
case "format:binary": {
|
|
891
|
+
addFormat(res, "binary", check.message, refs);
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
case "contentEncoding:base64": {
|
|
895
|
+
setResponseValueAndErrors(res, "contentEncoding", "base64", check.message, refs);
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "pattern:zod": {
|
|
899
|
+
addPattern(res, zodPatterns.base64, check.message, refs);
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
case "nanoid": {
|
|
906
|
+
addPattern(res, zodPatterns.nanoid, check.message, refs);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return res;
|
|
912
|
+
}
|
|
913
|
+
function escapeLiteralCheckValue(literal, refs) {
|
|
914
|
+
return refs.patternStrategy === "escape" ? escapeNonAlphaNumeric(literal) : literal;
|
|
915
|
+
}
|
|
916
|
+
const ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
|
|
917
|
+
function escapeNonAlphaNumeric(source) {
|
|
918
|
+
let result = "";
|
|
919
|
+
for (let i = 0; i < source.length; i++) {
|
|
920
|
+
if (!ALPHA_NUMERIC.has(source[i])) {
|
|
921
|
+
result += "\\";
|
|
922
|
+
}
|
|
923
|
+
result += source[i];
|
|
924
|
+
}
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
927
|
+
function addFormat(schema, value, message, refs) {
|
|
928
|
+
if (schema.format || schema.anyOf?.some((x) => x.format)) {
|
|
929
|
+
if (!schema.anyOf) {
|
|
930
|
+
schema.anyOf = [];
|
|
931
|
+
}
|
|
932
|
+
if (schema.format) {
|
|
933
|
+
schema.anyOf.push({
|
|
934
|
+
format: schema.format,
|
|
935
|
+
...schema.errorMessage && refs.errorMessages && {
|
|
936
|
+
errorMessage: { format: schema.errorMessage.format }
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
delete schema.format;
|
|
940
|
+
if (schema.errorMessage) {
|
|
941
|
+
delete schema.errorMessage.format;
|
|
942
|
+
if (Object.keys(schema.errorMessage).length === 0) {
|
|
943
|
+
delete schema.errorMessage;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
schema.anyOf.push({
|
|
948
|
+
format: value,
|
|
949
|
+
...message && refs.errorMessages && { errorMessage: { format: message } }
|
|
950
|
+
});
|
|
951
|
+
} else {
|
|
952
|
+
setResponseValueAndErrors(schema, "format", value, message, refs);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function addPattern(schema, regex, message, refs) {
|
|
956
|
+
if (schema.pattern || schema.allOf?.some((x) => x.pattern)) {
|
|
957
|
+
if (!schema.allOf) {
|
|
958
|
+
schema.allOf = [];
|
|
959
|
+
}
|
|
960
|
+
if (schema.pattern) {
|
|
961
|
+
schema.allOf.push({
|
|
962
|
+
pattern: schema.pattern,
|
|
963
|
+
...schema.errorMessage && refs.errorMessages && {
|
|
964
|
+
errorMessage: { pattern: schema.errorMessage.pattern }
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
delete schema.pattern;
|
|
968
|
+
if (schema.errorMessage) {
|
|
969
|
+
delete schema.errorMessage.pattern;
|
|
970
|
+
if (Object.keys(schema.errorMessage).length === 0) {
|
|
971
|
+
delete schema.errorMessage;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
schema.allOf.push({
|
|
976
|
+
pattern: stringifyRegExpWithFlags(regex, refs),
|
|
977
|
+
...message && refs.errorMessages && { errorMessage: { pattern: message } }
|
|
978
|
+
});
|
|
979
|
+
} else {
|
|
980
|
+
setResponseValueAndErrors(schema, "pattern", stringifyRegExpWithFlags(regex, refs), message, refs);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function stringifyRegExpWithFlags(regex, refs) {
|
|
984
|
+
if (!refs.applyRegexFlags || !regex.flags) {
|
|
985
|
+
return regex.source;
|
|
986
|
+
}
|
|
987
|
+
const flags = {
|
|
988
|
+
i: regex.flags.includes("i"),
|
|
989
|
+
m: regex.flags.includes("m"),
|
|
990
|
+
s: regex.flags.includes("s")
|
|
991
|
+
// `.` matches newlines
|
|
992
|
+
};
|
|
993
|
+
const source = flags.i ? regex.source.toLowerCase() : regex.source;
|
|
994
|
+
let pattern = "";
|
|
995
|
+
let isEscaped = false;
|
|
996
|
+
let inCharGroup = false;
|
|
997
|
+
let inCharRange = false;
|
|
998
|
+
for (let i = 0; i < source.length; i++) {
|
|
999
|
+
if (isEscaped) {
|
|
1000
|
+
pattern += source[i];
|
|
1001
|
+
isEscaped = false;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (flags.i) {
|
|
1005
|
+
if (inCharGroup) {
|
|
1006
|
+
if (source[i].match(/[a-z]/)) {
|
|
1007
|
+
if (inCharRange) {
|
|
1008
|
+
pattern += source[i];
|
|
1009
|
+
pattern += `${source[i - 2]}-${source[i]}`.toUpperCase();
|
|
1010
|
+
inCharRange = false;
|
|
1011
|
+
} else if (source[i + 1] === "-" && source[i + 2]?.match(/[a-z]/)) {
|
|
1012
|
+
pattern += source[i];
|
|
1013
|
+
inCharRange = true;
|
|
1014
|
+
} else {
|
|
1015
|
+
pattern += `${source[i]}${source[i].toUpperCase()}`;
|
|
1016
|
+
}
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
} else if (source[i].match(/[a-z]/)) {
|
|
1020
|
+
pattern += `[${source[i]}${source[i].toUpperCase()}]`;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (flags.m) {
|
|
1025
|
+
if (source[i] === "^") {
|
|
1026
|
+
pattern += `(^|(?<=[\r
|
|
1027
|
+
]))`;
|
|
1028
|
+
continue;
|
|
1029
|
+
} else if (source[i] === "$") {
|
|
1030
|
+
pattern += `($|(?=[\r
|
|
1031
|
+
]))`;
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (flags.s && source[i] === ".") {
|
|
1036
|
+
pattern += inCharGroup ? `${source[i]}\r
|
|
1037
|
+
` : `[${source[i]}\r
|
|
1038
|
+
]`;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
pattern += source[i];
|
|
1042
|
+
if (source[i] === "\\") {
|
|
1043
|
+
isEscaped = true;
|
|
1044
|
+
} else if (inCharGroup && source[i] === "]") {
|
|
1045
|
+
inCharGroup = false;
|
|
1046
|
+
} else if (!inCharGroup && source[i] === "[") {
|
|
1047
|
+
inCharGroup = true;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
new RegExp(pattern);
|
|
1052
|
+
} catch {
|
|
1053
|
+
console.warn(`Could not convert regex pattern at ${refs.currentPath.join("/")} to a flag-independent form! Falling back to the flag-ignorant source`);
|
|
1054
|
+
return regex.source;
|
|
1055
|
+
}
|
|
1056
|
+
return pattern;
|
|
1057
|
+
}
|
|
1058
|
+
function parseRecordDef(def, refs) {
|
|
1059
|
+
if (refs.target === "openAi") {
|
|
1060
|
+
console.warn("Warning: OpenAI may not support records in schemas! Try an array of key-value pairs instead.");
|
|
1061
|
+
}
|
|
1062
|
+
if (refs.target === "openApi3" && def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
|
|
1063
|
+
return {
|
|
1064
|
+
type: "object",
|
|
1065
|
+
required: def.keyType._def.values,
|
|
1066
|
+
properties: def.keyType._def.values.reduce((acc, key) => ({
|
|
1067
|
+
...acc,
|
|
1068
|
+
[key]: parseDef(def.valueType._def, {
|
|
1069
|
+
...refs,
|
|
1070
|
+
currentPath: [...refs.currentPath, "properties", key]
|
|
1071
|
+
}) ?? parseAnyDef(refs)
|
|
1072
|
+
}), {}),
|
|
1073
|
+
additionalProperties: refs.rejectedAdditionalProperties
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const schema = {
|
|
1077
|
+
type: "object",
|
|
1078
|
+
additionalProperties: parseDef(def.valueType._def, {
|
|
1079
|
+
...refs,
|
|
1080
|
+
currentPath: [...refs.currentPath, "additionalProperties"]
|
|
1081
|
+
}) ?? refs.allowedAdditionalProperties
|
|
1082
|
+
};
|
|
1083
|
+
if (refs.target === "openApi3") {
|
|
1084
|
+
return schema;
|
|
1085
|
+
}
|
|
1086
|
+
if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodString && def.keyType._def.checks?.length) {
|
|
1087
|
+
const { type, ...keyType } = parseStringDef(def.keyType._def, refs);
|
|
1088
|
+
return {
|
|
1089
|
+
...schema,
|
|
1090
|
+
propertyNames: keyType
|
|
1091
|
+
};
|
|
1092
|
+
} else if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
|
|
1093
|
+
return {
|
|
1094
|
+
...schema,
|
|
1095
|
+
propertyNames: {
|
|
1096
|
+
enum: def.keyType._def.values
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
} else if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodBranded && def.keyType._def.type._def.typeName === ZodFirstPartyTypeKind.ZodString && def.keyType._def.type._def.checks?.length) {
|
|
1100
|
+
const { type, ...keyType } = parseBrandedDef(def.keyType._def, refs);
|
|
1101
|
+
return {
|
|
1102
|
+
...schema,
|
|
1103
|
+
propertyNames: keyType
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
return schema;
|
|
1107
|
+
}
|
|
1108
|
+
function parseMapDef(def, refs) {
|
|
1109
|
+
if (refs.mapStrategy === "record") {
|
|
1110
|
+
return parseRecordDef(def, refs);
|
|
1111
|
+
}
|
|
1112
|
+
const keys = parseDef(def.keyType._def, {
|
|
1113
|
+
...refs,
|
|
1114
|
+
currentPath: [...refs.currentPath, "items", "items", "0"]
|
|
1115
|
+
}) || parseAnyDef(refs);
|
|
1116
|
+
const values = parseDef(def.valueType._def, {
|
|
1117
|
+
...refs,
|
|
1118
|
+
currentPath: [...refs.currentPath, "items", "items", "1"]
|
|
1119
|
+
}) || parseAnyDef(refs);
|
|
1120
|
+
return {
|
|
1121
|
+
type: "array",
|
|
1122
|
+
maxItems: 125,
|
|
1123
|
+
items: {
|
|
1124
|
+
type: "array",
|
|
1125
|
+
items: [keys, values],
|
|
1126
|
+
minItems: 2,
|
|
1127
|
+
maxItems: 2
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function parseNativeEnumDef(def) {
|
|
1132
|
+
const object = def.values;
|
|
1133
|
+
const actualKeys = Object.keys(def.values).filter((key) => {
|
|
1134
|
+
return typeof object[object[key]] !== "number";
|
|
1135
|
+
});
|
|
1136
|
+
const actualValues = actualKeys.map((key) => object[key]);
|
|
1137
|
+
const parsedTypes = Array.from(new Set(actualValues.map((values) => typeof values)));
|
|
1138
|
+
return {
|
|
1139
|
+
type: parsedTypes.length === 1 ? parsedTypes[0] === "string" ? "string" : "number" : ["string", "number"],
|
|
1140
|
+
enum: actualValues
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function parseNeverDef(refs) {
|
|
1144
|
+
return refs.target === "openAi" ? void 0 : {
|
|
1145
|
+
not: parseAnyDef({
|
|
1146
|
+
...refs,
|
|
1147
|
+
currentPath: [...refs.currentPath, "not"]
|
|
1148
|
+
})
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function parseNullDef(refs) {
|
|
1152
|
+
return refs.target === "openApi3" ? {
|
|
1153
|
+
enum: ["null"],
|
|
1154
|
+
nullable: true
|
|
1155
|
+
} : {
|
|
1156
|
+
type: "null"
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const primitiveMappings = {
|
|
1160
|
+
ZodString: "string",
|
|
1161
|
+
ZodNumber: "number",
|
|
1162
|
+
ZodBigInt: "integer",
|
|
1163
|
+
ZodBoolean: "boolean",
|
|
1164
|
+
ZodNull: "null"
|
|
1165
|
+
};
|
|
1166
|
+
function parseUnionDef(def, refs) {
|
|
1167
|
+
if (refs.target === "openApi3")
|
|
1168
|
+
return asAnyOf(def, refs);
|
|
1169
|
+
const options = def.options instanceof Map ? Array.from(def.options.values()) : def.options;
|
|
1170
|
+
if (options.every((x) => x._def.typeName in primitiveMappings && (!x._def.checks || !x._def.checks.length))) {
|
|
1171
|
+
const types = options.reduce((types2, x) => {
|
|
1172
|
+
const type = primitiveMappings[x._def.typeName];
|
|
1173
|
+
return type && !types2.includes(type) ? [...types2, type] : types2;
|
|
1174
|
+
}, []);
|
|
1175
|
+
return {
|
|
1176
|
+
type: types.length > 1 ? types : types[0]
|
|
1177
|
+
};
|
|
1178
|
+
} else if (options.every((x) => x._def.typeName === "ZodLiteral" && !x.description)) {
|
|
1179
|
+
const types = options.reduce((acc, x) => {
|
|
1180
|
+
const type = typeof x._def.value;
|
|
1181
|
+
switch (type) {
|
|
1182
|
+
case "string":
|
|
1183
|
+
case "number":
|
|
1184
|
+
case "boolean":
|
|
1185
|
+
return [...acc, type];
|
|
1186
|
+
case "bigint":
|
|
1187
|
+
return [...acc, "integer"];
|
|
1188
|
+
case "object":
|
|
1189
|
+
if (x._def.value === null)
|
|
1190
|
+
return [...acc, "null"];
|
|
1191
|
+
case "symbol":
|
|
1192
|
+
case "undefined":
|
|
1193
|
+
case "function":
|
|
1194
|
+
default:
|
|
1195
|
+
return acc;
|
|
1196
|
+
}
|
|
1197
|
+
}, []);
|
|
1198
|
+
if (types.length === options.length) {
|
|
1199
|
+
const uniqueTypes = types.filter((x, i, a) => a.indexOf(x) === i);
|
|
1200
|
+
return {
|
|
1201
|
+
type: uniqueTypes.length > 1 ? uniqueTypes : uniqueTypes[0],
|
|
1202
|
+
enum: options.reduce((acc, x) => {
|
|
1203
|
+
return acc.includes(x._def.value) ? acc : [...acc, x._def.value];
|
|
1204
|
+
}, [])
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
} else if (options.every((x) => x._def.typeName === "ZodEnum")) {
|
|
1208
|
+
return {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
enum: options.reduce((acc, x) => [
|
|
1211
|
+
...acc,
|
|
1212
|
+
...x._def.values.filter((x2) => !acc.includes(x2))
|
|
1213
|
+
], [])
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
return asAnyOf(def, refs);
|
|
1217
|
+
}
|
|
1218
|
+
const asAnyOf = (def, refs) => {
|
|
1219
|
+
const anyOf = (def.options instanceof Map ? Array.from(def.options.values()) : def.options).map((x, i) => parseDef(x._def, {
|
|
1220
|
+
...refs,
|
|
1221
|
+
currentPath: [...refs.currentPath, "anyOf", `${i}`]
|
|
1222
|
+
})).filter((x) => !!x && (!refs.strictUnions || typeof x === "object" && Object.keys(x).length > 0));
|
|
1223
|
+
return anyOf.length ? { anyOf } : void 0;
|
|
1224
|
+
};
|
|
1225
|
+
function parseNullableDef(def, refs) {
|
|
1226
|
+
if (["ZodString", "ZodNumber", "ZodBigInt", "ZodBoolean", "ZodNull"].includes(def.innerType._def.typeName) && (!def.innerType._def.checks || !def.innerType._def.checks.length)) {
|
|
1227
|
+
if (refs.target === "openApi3") {
|
|
1228
|
+
return {
|
|
1229
|
+
type: primitiveMappings[def.innerType._def.typeName],
|
|
1230
|
+
nullable: true
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
type: [
|
|
1235
|
+
primitiveMappings[def.innerType._def.typeName],
|
|
1236
|
+
"null"
|
|
1237
|
+
]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (refs.target === "openApi3") {
|
|
1241
|
+
const base2 = parseDef(def.innerType._def, {
|
|
1242
|
+
...refs,
|
|
1243
|
+
currentPath: [...refs.currentPath]
|
|
1244
|
+
});
|
|
1245
|
+
if (base2 && "$ref" in base2)
|
|
1246
|
+
return { allOf: [base2], nullable: true };
|
|
1247
|
+
return base2 && { ...base2, nullable: true };
|
|
1248
|
+
}
|
|
1249
|
+
const base = parseDef(def.innerType._def, {
|
|
1250
|
+
...refs,
|
|
1251
|
+
currentPath: [...refs.currentPath, "anyOf", "0"]
|
|
1252
|
+
});
|
|
1253
|
+
return base && { anyOf: [base, { type: "null" }] };
|
|
1254
|
+
}
|
|
1255
|
+
function parseNumberDef(def, refs) {
|
|
1256
|
+
const res = {
|
|
1257
|
+
type: "number"
|
|
1258
|
+
};
|
|
1259
|
+
if (!def.checks)
|
|
1260
|
+
return res;
|
|
1261
|
+
for (const check of def.checks) {
|
|
1262
|
+
switch (check.kind) {
|
|
1263
|
+
case "int":
|
|
1264
|
+
res.type = "integer";
|
|
1265
|
+
addErrorMessage(res, "type", check.message, refs);
|
|
1266
|
+
break;
|
|
1267
|
+
case "min":
|
|
1268
|
+
if (refs.target === "jsonSchema7") {
|
|
1269
|
+
if (check.inclusive) {
|
|
1270
|
+
setResponseValueAndErrors(res, "minimum", check.value, check.message, refs);
|
|
1271
|
+
} else {
|
|
1272
|
+
setResponseValueAndErrors(res, "exclusiveMinimum", check.value, check.message, refs);
|
|
1273
|
+
}
|
|
1274
|
+
} else {
|
|
1275
|
+
if (!check.inclusive) {
|
|
1276
|
+
res.exclusiveMinimum = true;
|
|
1277
|
+
}
|
|
1278
|
+
setResponseValueAndErrors(res, "minimum", check.value, check.message, refs);
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
case "max":
|
|
1282
|
+
if (refs.target === "jsonSchema7") {
|
|
1283
|
+
if (check.inclusive) {
|
|
1284
|
+
setResponseValueAndErrors(res, "maximum", check.value, check.message, refs);
|
|
1285
|
+
} else {
|
|
1286
|
+
setResponseValueAndErrors(res, "exclusiveMaximum", check.value, check.message, refs);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
if (!check.inclusive) {
|
|
1290
|
+
res.exclusiveMaximum = true;
|
|
1291
|
+
}
|
|
1292
|
+
setResponseValueAndErrors(res, "maximum", check.value, check.message, refs);
|
|
1293
|
+
}
|
|
1294
|
+
break;
|
|
1295
|
+
case "multipleOf":
|
|
1296
|
+
setResponseValueAndErrors(res, "multipleOf", check.value, check.message, refs);
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return res;
|
|
1301
|
+
}
|
|
1302
|
+
function parseObjectDef(def, refs) {
|
|
1303
|
+
const forceOptionalIntoNullable = refs.target === "openAi";
|
|
1304
|
+
const result = {
|
|
1305
|
+
type: "object",
|
|
1306
|
+
properties: {}
|
|
1307
|
+
};
|
|
1308
|
+
const required = [];
|
|
1309
|
+
const shape = def.shape();
|
|
1310
|
+
for (const propName in shape) {
|
|
1311
|
+
let propDef = shape[propName];
|
|
1312
|
+
if (propDef === void 0 || propDef._def === void 0) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
let propOptional = safeIsOptional(propDef);
|
|
1316
|
+
if (propOptional && forceOptionalIntoNullable) {
|
|
1317
|
+
if (propDef._def.typeName === "ZodOptional") {
|
|
1318
|
+
propDef = propDef._def.innerType;
|
|
1319
|
+
}
|
|
1320
|
+
if (!propDef.isNullable()) {
|
|
1321
|
+
propDef = propDef.nullable();
|
|
1322
|
+
}
|
|
1323
|
+
propOptional = false;
|
|
1324
|
+
}
|
|
1325
|
+
const parsedDef = parseDef(propDef._def, {
|
|
1326
|
+
...refs,
|
|
1327
|
+
currentPath: [...refs.currentPath, "properties", propName],
|
|
1328
|
+
propertyPath: [...refs.currentPath, "properties", propName]
|
|
1329
|
+
});
|
|
1330
|
+
if (parsedDef === void 0) {
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
result.properties[propName] = parsedDef;
|
|
1334
|
+
if (!propOptional) {
|
|
1335
|
+
required.push(propName);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (required.length) {
|
|
1339
|
+
result.required = required;
|
|
1340
|
+
}
|
|
1341
|
+
const additionalProperties = decideAdditionalProperties(def, refs);
|
|
1342
|
+
if (additionalProperties !== void 0) {
|
|
1343
|
+
result.additionalProperties = additionalProperties;
|
|
1344
|
+
}
|
|
1345
|
+
return result;
|
|
1346
|
+
}
|
|
1347
|
+
function decideAdditionalProperties(def, refs) {
|
|
1348
|
+
if (def.catchall._def.typeName !== "ZodNever") {
|
|
1349
|
+
return parseDef(def.catchall._def, {
|
|
1350
|
+
...refs,
|
|
1351
|
+
currentPath: [...refs.currentPath, "additionalProperties"]
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
switch (def.unknownKeys) {
|
|
1355
|
+
case "passthrough":
|
|
1356
|
+
return refs.allowedAdditionalProperties;
|
|
1357
|
+
case "strict":
|
|
1358
|
+
return refs.rejectedAdditionalProperties;
|
|
1359
|
+
case "strip":
|
|
1360
|
+
return refs.removeAdditionalStrategy === "strict" ? refs.allowedAdditionalProperties : refs.rejectedAdditionalProperties;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function safeIsOptional(schema) {
|
|
1364
|
+
try {
|
|
1365
|
+
return schema.isOptional();
|
|
1366
|
+
} catch {
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const parseOptionalDef = (def, refs) => {
|
|
1371
|
+
if (refs.currentPath.toString() === refs.propertyPath?.toString()) {
|
|
1372
|
+
return parseDef(def.innerType._def, refs);
|
|
1373
|
+
}
|
|
1374
|
+
const innerSchema = parseDef(def.innerType._def, {
|
|
1375
|
+
...refs,
|
|
1376
|
+
currentPath: [...refs.currentPath, "anyOf", "1"]
|
|
1377
|
+
});
|
|
1378
|
+
return innerSchema ? {
|
|
1379
|
+
anyOf: [
|
|
1380
|
+
{
|
|
1381
|
+
not: parseAnyDef(refs)
|
|
1382
|
+
},
|
|
1383
|
+
innerSchema
|
|
1384
|
+
]
|
|
1385
|
+
} : parseAnyDef(refs);
|
|
1386
|
+
};
|
|
1387
|
+
const parsePipelineDef = (def, refs) => {
|
|
1388
|
+
if (refs.pipeStrategy === "input") {
|
|
1389
|
+
return parseDef(def.in._def, refs);
|
|
1390
|
+
} else if (refs.pipeStrategy === "output") {
|
|
1391
|
+
return parseDef(def.out._def, refs);
|
|
1392
|
+
}
|
|
1393
|
+
const a = parseDef(def.in._def, {
|
|
1394
|
+
...refs,
|
|
1395
|
+
currentPath: [...refs.currentPath, "allOf", "0"]
|
|
1396
|
+
});
|
|
1397
|
+
const b = parseDef(def.out._def, {
|
|
1398
|
+
...refs,
|
|
1399
|
+
currentPath: [...refs.currentPath, "allOf", a ? "1" : "0"]
|
|
1400
|
+
});
|
|
1401
|
+
return {
|
|
1402
|
+
allOf: [a, b].filter((x) => x !== void 0)
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
function parsePromiseDef(def, refs) {
|
|
1406
|
+
return parseDef(def.type._def, refs);
|
|
1407
|
+
}
|
|
1408
|
+
function parseSetDef(def, refs) {
|
|
1409
|
+
const items = parseDef(def.valueType._def, {
|
|
1410
|
+
...refs,
|
|
1411
|
+
currentPath: [...refs.currentPath, "items"]
|
|
1412
|
+
});
|
|
1413
|
+
const schema = {
|
|
1414
|
+
type: "array",
|
|
1415
|
+
uniqueItems: true,
|
|
1416
|
+
items
|
|
1417
|
+
};
|
|
1418
|
+
if (def.minSize) {
|
|
1419
|
+
setResponseValueAndErrors(schema, "minItems", def.minSize.value, def.minSize.message, refs);
|
|
1420
|
+
}
|
|
1421
|
+
if (def.maxSize) {
|
|
1422
|
+
setResponseValueAndErrors(schema, "maxItems", def.maxSize.value, def.maxSize.message, refs);
|
|
1423
|
+
}
|
|
1424
|
+
return schema;
|
|
1425
|
+
}
|
|
1426
|
+
function parseTupleDef(def, refs) {
|
|
1427
|
+
if (def.rest) {
|
|
1428
|
+
return {
|
|
1429
|
+
type: "array",
|
|
1430
|
+
minItems: def.items.length,
|
|
1431
|
+
items: def.items.map((x, i) => parseDef(x._def, {
|
|
1432
|
+
...refs,
|
|
1433
|
+
currentPath: [...refs.currentPath, "items", `${i}`]
|
|
1434
|
+
})).reduce((acc, x) => x === void 0 ? acc : [...acc, x], []),
|
|
1435
|
+
additionalItems: parseDef(def.rest._def, {
|
|
1436
|
+
...refs,
|
|
1437
|
+
currentPath: [...refs.currentPath, "additionalItems"]
|
|
1438
|
+
})
|
|
1439
|
+
};
|
|
1440
|
+
} else {
|
|
1441
|
+
return {
|
|
1442
|
+
type: "array",
|
|
1443
|
+
minItems: def.items.length,
|
|
1444
|
+
maxItems: def.items.length,
|
|
1445
|
+
items: def.items.map((x, i) => parseDef(x._def, {
|
|
1446
|
+
...refs,
|
|
1447
|
+
currentPath: [...refs.currentPath, "items", `${i}`]
|
|
1448
|
+
})).reduce((acc, x) => x === void 0 ? acc : [...acc, x], [])
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function parseUndefinedDef(refs) {
|
|
1453
|
+
return {
|
|
1454
|
+
not: parseAnyDef(refs)
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function parseUnknownDef(refs) {
|
|
1458
|
+
return parseAnyDef(refs);
|
|
1459
|
+
}
|
|
1460
|
+
const parseReadonlyDef = (def, refs) => {
|
|
1461
|
+
return parseDef(def.innerType._def, refs);
|
|
1462
|
+
};
|
|
1463
|
+
const selectParser = (def, typeName, refs) => {
|
|
1464
|
+
switch (typeName) {
|
|
1465
|
+
case ZodFirstPartyTypeKind.ZodString:
|
|
1466
|
+
return parseStringDef(def, refs);
|
|
1467
|
+
case ZodFirstPartyTypeKind.ZodNumber:
|
|
1468
|
+
return parseNumberDef(def, refs);
|
|
1469
|
+
case ZodFirstPartyTypeKind.ZodObject:
|
|
1470
|
+
return parseObjectDef(def, refs);
|
|
1471
|
+
case ZodFirstPartyTypeKind.ZodBigInt:
|
|
1472
|
+
return parseBigintDef(def, refs);
|
|
1473
|
+
case ZodFirstPartyTypeKind.ZodBoolean:
|
|
1474
|
+
return parseBooleanDef();
|
|
1475
|
+
case ZodFirstPartyTypeKind.ZodDate:
|
|
1476
|
+
return parseDateDef(def, refs);
|
|
1477
|
+
case ZodFirstPartyTypeKind.ZodUndefined:
|
|
1478
|
+
return parseUndefinedDef(refs);
|
|
1479
|
+
case ZodFirstPartyTypeKind.ZodNull:
|
|
1480
|
+
return parseNullDef(refs);
|
|
1481
|
+
case ZodFirstPartyTypeKind.ZodArray:
|
|
1482
|
+
return parseArrayDef(def, refs);
|
|
1483
|
+
case ZodFirstPartyTypeKind.ZodUnion:
|
|
1484
|
+
case ZodFirstPartyTypeKind.ZodDiscriminatedUnion:
|
|
1485
|
+
return parseUnionDef(def, refs);
|
|
1486
|
+
case ZodFirstPartyTypeKind.ZodIntersection:
|
|
1487
|
+
return parseIntersectionDef(def, refs);
|
|
1488
|
+
case ZodFirstPartyTypeKind.ZodTuple:
|
|
1489
|
+
return parseTupleDef(def, refs);
|
|
1490
|
+
case ZodFirstPartyTypeKind.ZodRecord:
|
|
1491
|
+
return parseRecordDef(def, refs);
|
|
1492
|
+
case ZodFirstPartyTypeKind.ZodLiteral:
|
|
1493
|
+
return parseLiteralDef(def, refs);
|
|
1494
|
+
case ZodFirstPartyTypeKind.ZodEnum:
|
|
1495
|
+
return parseEnumDef(def);
|
|
1496
|
+
case ZodFirstPartyTypeKind.ZodNativeEnum:
|
|
1497
|
+
return parseNativeEnumDef(def);
|
|
1498
|
+
case ZodFirstPartyTypeKind.ZodNullable:
|
|
1499
|
+
return parseNullableDef(def, refs);
|
|
1500
|
+
case ZodFirstPartyTypeKind.ZodOptional:
|
|
1501
|
+
return parseOptionalDef(def, refs);
|
|
1502
|
+
case ZodFirstPartyTypeKind.ZodMap:
|
|
1503
|
+
return parseMapDef(def, refs);
|
|
1504
|
+
case ZodFirstPartyTypeKind.ZodSet:
|
|
1505
|
+
return parseSetDef(def, refs);
|
|
1506
|
+
case ZodFirstPartyTypeKind.ZodLazy:
|
|
1507
|
+
return () => def.getter()._def;
|
|
1508
|
+
case ZodFirstPartyTypeKind.ZodPromise:
|
|
1509
|
+
return parsePromiseDef(def, refs);
|
|
1510
|
+
case ZodFirstPartyTypeKind.ZodNaN:
|
|
1511
|
+
case ZodFirstPartyTypeKind.ZodNever:
|
|
1512
|
+
return parseNeverDef(refs);
|
|
1513
|
+
case ZodFirstPartyTypeKind.ZodEffects:
|
|
1514
|
+
return parseEffectsDef(def, refs);
|
|
1515
|
+
case ZodFirstPartyTypeKind.ZodAny:
|
|
1516
|
+
return parseAnyDef(refs);
|
|
1517
|
+
case ZodFirstPartyTypeKind.ZodUnknown:
|
|
1518
|
+
return parseUnknownDef(refs);
|
|
1519
|
+
case ZodFirstPartyTypeKind.ZodDefault:
|
|
1520
|
+
return parseDefaultDef(def, refs);
|
|
1521
|
+
case ZodFirstPartyTypeKind.ZodBranded:
|
|
1522
|
+
return parseBrandedDef(def, refs);
|
|
1523
|
+
case ZodFirstPartyTypeKind.ZodReadonly:
|
|
1524
|
+
return parseReadonlyDef(def, refs);
|
|
1525
|
+
case ZodFirstPartyTypeKind.ZodCatch:
|
|
1526
|
+
return parseCatchDef(def, refs);
|
|
1527
|
+
case ZodFirstPartyTypeKind.ZodPipeline:
|
|
1528
|
+
return parsePipelineDef(def, refs);
|
|
1529
|
+
case ZodFirstPartyTypeKind.ZodFunction:
|
|
1530
|
+
case ZodFirstPartyTypeKind.ZodVoid:
|
|
1531
|
+
case ZodFirstPartyTypeKind.ZodSymbol:
|
|
1532
|
+
return void 0;
|
|
1533
|
+
default:
|
|
1534
|
+
return /* @__PURE__ */ ((_) => void 0)();
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
function parseDef(def, refs, forceResolution = false) {
|
|
1538
|
+
const seenItem = refs.seen.get(def);
|
|
1539
|
+
if (refs.override) {
|
|
1540
|
+
const overrideResult = refs.override?.(def, refs, seenItem, forceResolution);
|
|
1541
|
+
if (overrideResult !== ignoreOverride) {
|
|
1542
|
+
return overrideResult;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (seenItem && !forceResolution) {
|
|
1546
|
+
const seenSchema = get$ref(seenItem, refs);
|
|
1547
|
+
if (seenSchema !== void 0) {
|
|
1548
|
+
return seenSchema;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const newItem = { def, path: refs.currentPath, jsonSchema: void 0 };
|
|
1552
|
+
refs.seen.set(def, newItem);
|
|
1553
|
+
const jsonSchemaOrGetter = selectParser(def, def.typeName, refs);
|
|
1554
|
+
const jsonSchema = typeof jsonSchemaOrGetter === "function" ? parseDef(jsonSchemaOrGetter(), refs) : jsonSchemaOrGetter;
|
|
1555
|
+
if (jsonSchema) {
|
|
1556
|
+
addMeta(def, refs, jsonSchema);
|
|
1557
|
+
}
|
|
1558
|
+
if (refs.postProcess) {
|
|
1559
|
+
const postProcessResult = refs.postProcess(jsonSchema, def, refs);
|
|
1560
|
+
newItem.jsonSchema = jsonSchema;
|
|
1561
|
+
return postProcessResult;
|
|
1562
|
+
}
|
|
1563
|
+
newItem.jsonSchema = jsonSchema;
|
|
1564
|
+
return jsonSchema;
|
|
1565
|
+
}
|
|
1566
|
+
const get$ref = (item, refs) => {
|
|
1567
|
+
switch (refs.$refStrategy) {
|
|
1568
|
+
case "root":
|
|
1569
|
+
return { $ref: item.path.join("/") };
|
|
1570
|
+
case "relative":
|
|
1571
|
+
return { $ref: getRelativePath(refs.currentPath, item.path) };
|
|
1572
|
+
case "none":
|
|
1573
|
+
case "seen": {
|
|
1574
|
+
if (item.path.length < refs.currentPath.length && item.path.every((value, index) => refs.currentPath[index] === value)) {
|
|
1575
|
+
console.warn(`Recursive reference detected at ${refs.currentPath.join("/")}! Defaulting to any`);
|
|
1576
|
+
return parseAnyDef(refs);
|
|
1577
|
+
}
|
|
1578
|
+
return refs.$refStrategy === "seen" ? parseAnyDef(refs) : void 0;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const addMeta = (def, refs, jsonSchema) => {
|
|
1583
|
+
if (def.description) {
|
|
1584
|
+
jsonSchema.description = def.description;
|
|
1585
|
+
if (refs.markdownDescription) {
|
|
1586
|
+
jsonSchema.markdownDescription = def.description;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return jsonSchema;
|
|
1590
|
+
};
|
|
1591
|
+
const zodToJsonSchema = (schema, options) => {
|
|
1592
|
+
const refs = getRefs(options);
|
|
1593
|
+
let definitions = void 0;
|
|
1594
|
+
const main = parseDef(
|
|
1595
|
+
schema._def,
|
|
1596
|
+
refs,
|
|
1597
|
+
false
|
|
1598
|
+
) ?? parseAnyDef(refs);
|
|
1599
|
+
if (refs.flags.hasReferencedOpenAiAnyType) {
|
|
1600
|
+
if (!definitions) {
|
|
1601
|
+
definitions = {};
|
|
1602
|
+
}
|
|
1603
|
+
if (!definitions[refs.openAiAnyTypeName]) {
|
|
1604
|
+
definitions[refs.openAiAnyTypeName] = {
|
|
1605
|
+
// Skipping "object" as no properties can be defined and additionalProperties must be "false"
|
|
1606
|
+
type: ["string", "number", "integer", "boolean", "array", "null"],
|
|
1607
|
+
items: {
|
|
1608
|
+
$ref: refs.$refStrategy === "relative" ? "1" : [
|
|
1609
|
+
...refs.basePath,
|
|
1610
|
+
refs.definitionPath,
|
|
1611
|
+
refs.openAiAnyTypeName
|
|
1612
|
+
].join("/")
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const combined = definitions ? {
|
|
1618
|
+
...main,
|
|
1619
|
+
[refs.definitionPath]: definitions
|
|
1620
|
+
} : main;
|
|
1621
|
+
if (refs.target === "jsonSchema7") {
|
|
1622
|
+
combined.$schema = "http://json-schema.org/draft-07/schema#";
|
|
1623
|
+
} else if (refs.target === "jsonSchema2019-09" || refs.target === "openAi") {
|
|
1624
|
+
combined.$schema = "https://json-schema.org/draft/2019-09/schema#";
|
|
1625
|
+
}
|
|
1626
|
+
if (refs.target === "openAi" && ("anyOf" in combined || "oneOf" in combined || "allOf" in combined || "type" in combined && Array.isArray(combined.type))) {
|
|
1627
|
+
console.warn("Warning: OpenAI may not support schemas with unions as roots! Try wrapping it in an object property.");
|
|
1628
|
+
}
|
|
1629
|
+
return combined;
|
|
1630
|
+
};
|
|
1631
|
+
const UNLIMITED = Number.MAX_SAFE_INTEGER;
|
|
1632
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
1633
|
+
const nonEmptyString = (description) => z.string().trim().min(1).describe(description);
|
|
1634
|
+
const ReactionSchema = z.object({
|
|
1635
|
+
type: z.string(),
|
|
1636
|
+
emoji: z.string().optional(),
|
|
1637
|
+
fromHandle: z.string(),
|
|
1638
|
+
isRemoval: z.boolean(),
|
|
1639
|
+
targetMessageGuid: z.string(),
|
|
1640
|
+
targetMessagePart: z.number().int()
|
|
1641
|
+
});
|
|
1642
|
+
const ReplyContextSchema = z.object({
|
|
1643
|
+
replyToGuid: z.string(),
|
|
1644
|
+
replyToText: z.string().nullable().optional()
|
|
1645
|
+
});
|
|
1646
|
+
const AttachmentSchema = z.object({
|
|
1647
|
+
filename: z.string(),
|
|
1648
|
+
mimeType: z.string().nullable(),
|
|
1649
|
+
transferName: z.string().nullable(),
|
|
1650
|
+
totalBytes: z.number().int()
|
|
1651
|
+
});
|
|
1652
|
+
const MessageSchema = z.object({
|
|
1653
|
+
id: z.number().int(),
|
|
1654
|
+
guid: z.string(),
|
|
1655
|
+
text: z.string().nullable(),
|
|
1656
|
+
handle: z.string(),
|
|
1657
|
+
displayName: z.string().optional(),
|
|
1658
|
+
isFromMe: z.boolean(),
|
|
1659
|
+
date: z.string(),
|
|
1660
|
+
dateRead: z.string().nullable(),
|
|
1661
|
+
dateDelivered: z.string().nullable(),
|
|
1662
|
+
isRead: z.boolean(),
|
|
1663
|
+
isDelivered: z.boolean(),
|
|
1664
|
+
chatId: z.string(),
|
|
1665
|
+
service: z.enum(["iMessage", "SMS"]),
|
|
1666
|
+
isReaction: z.boolean(),
|
|
1667
|
+
reaction: ReactionSchema.optional(),
|
|
1668
|
+
isReply: z.boolean(),
|
|
1669
|
+
replyTo: ReplyContextSchema.optional(),
|
|
1670
|
+
reactions: z.array(ReactionSchema).optional(),
|
|
1671
|
+
richContentType: z.string().optional(),
|
|
1672
|
+
richContentSummary: z.string().optional(),
|
|
1673
|
+
isEdited: z.boolean(),
|
|
1674
|
+
isRetracted: z.boolean(),
|
|
1675
|
+
hasAttachments: z.boolean(),
|
|
1676
|
+
attachments: z.array(AttachmentSchema).optional()
|
|
1677
|
+
});
|
|
1678
|
+
const ConversationSchema = z.object({
|
|
1679
|
+
chatId: z.string(),
|
|
1680
|
+
chatIdentifier: z.string(),
|
|
1681
|
+
displayName: z.string().nullable(),
|
|
1682
|
+
rawIdentifier: z.string(),
|
|
1683
|
+
participants: z.array(z.string()),
|
|
1684
|
+
lastMessageDate: z.string().nullable(),
|
|
1685
|
+
lastMessageSnippet: z.string().nullable(),
|
|
1686
|
+
unreadCount: z.number().int(),
|
|
1687
|
+
threadSlug: z.string(),
|
|
1688
|
+
isGroupChat: z.boolean(),
|
|
1689
|
+
serviceType: z.enum(["iMessage", "SMS"])
|
|
1690
|
+
});
|
|
1691
|
+
const GetMessagesSchema = z.object({
|
|
1692
|
+
limit: z.number().int().min(0).default(20).describe(
|
|
1693
|
+
"Number of messages to retrieve. 0 = unlimited (bounded by tool safety limits). Default 20."
|
|
1694
|
+
),
|
|
1695
|
+
chatIdentifier: nonEmptyString("Phone number, email, or chat ID to filter by").optional(),
|
|
1696
|
+
threadSlug: nonEmptyString("Thread slug from list_conversations").optional(),
|
|
1697
|
+
beforeMessageId: z.number().int().positive().optional().describe("Pagination cursor. Pass `oldestMessageId` from a previous response.")
|
|
1698
|
+
});
|
|
1699
|
+
const GetMessagesOutputSchema = z.object({
|
|
1700
|
+
messages: z.array(MessageSchema),
|
|
1701
|
+
count: z.number().int(),
|
|
1702
|
+
hasMore: z.boolean(),
|
|
1703
|
+
oldestMessageId: z.number().int().optional()
|
|
1704
|
+
});
|
|
1705
|
+
const ExportMessagesSchema = z.object({
|
|
1706
|
+
chatIdentifier: nonEmptyString("Phone number, email, or chat ID").optional(),
|
|
1707
|
+
threadSlug: nonEmptyString("Thread slug from list_conversations").optional(),
|
|
1708
|
+
format: z.enum(["markdown", "csv", "json", "ndjson"]).default("markdown"),
|
|
1709
|
+
outputPath: nonEmptyString(
|
|
1710
|
+
"Absolute path. Parent directory must exist; file will be created/overwritten."
|
|
1711
|
+
),
|
|
1712
|
+
since: nonEmptyString("Earliest date, ISO or relative like '1 year ago'").optional(),
|
|
1713
|
+
until: nonEmptyString("Latest date, ISO or relative like 'yesterday'").optional(),
|
|
1714
|
+
pageSize: z.number().int().min(100).max(5e3).default(1e3)
|
|
1715
|
+
});
|
|
1716
|
+
const ExportMessagesOutputSchema = z.object({
|
|
1717
|
+
count: z.number().int(),
|
|
1718
|
+
sizeBytes: z.number().int(),
|
|
1719
|
+
savedTo: z.string(),
|
|
1720
|
+
format: z.string(),
|
|
1721
|
+
oldest: z.string().nullable(),
|
|
1722
|
+
newest: z.string().nullable(),
|
|
1723
|
+
durationMs: z.number()
|
|
1724
|
+
});
|
|
1725
|
+
const GetUnreadMessagesSchema = z.object({
|
|
1726
|
+
limit: z.number().int().min(0).optional().describe("Max unread messages. 0 = unlimited. Default 100.")
|
|
1727
|
+
});
|
|
1728
|
+
const GetUnreadMessagesOutputSchema = z.object({
|
|
1729
|
+
messages: z.array(MessageSchema),
|
|
1730
|
+
count: z.number().int(),
|
|
1731
|
+
hasMore: z.boolean(),
|
|
1732
|
+
nextOffset: z.number().int().nullable()
|
|
1733
|
+
});
|
|
1734
|
+
const SendMessageSchema = z.object({
|
|
1735
|
+
recipient: nonEmptyString("Phone number or email address to send to").optional(),
|
|
1736
|
+
threadSlug: nonEmptyString("Thread slug from list_conversations").optional(),
|
|
1737
|
+
message: nonEmptyString("Message text to send"),
|
|
1738
|
+
attachments: z.array(nonEmptyString("Absolute file path to attach")).optional().describe(
|
|
1739
|
+
"Absolute file paths to attach. Each is sent in a follow-up AppleScript call after the text. Use sparingly — large files may be silently rate-limited by Messages.app."
|
|
1740
|
+
)
|
|
1741
|
+
});
|
|
1742
|
+
const SendMessageOutputSchema = z.object({
|
|
1743
|
+
success: z.boolean(),
|
|
1744
|
+
target: z.string().optional(),
|
|
1745
|
+
error: z.string().optional(),
|
|
1746
|
+
timestamp: z.string().nullable().optional(),
|
|
1747
|
+
threadSlug: z.string().optional(),
|
|
1748
|
+
lastMessageId: z.number().int().optional(),
|
|
1749
|
+
attachments: z.array(
|
|
1750
|
+
z.object({
|
|
1751
|
+
path: z.string(),
|
|
1752
|
+
success: z.boolean(),
|
|
1753
|
+
error: z.string().optional()
|
|
1754
|
+
})
|
|
1755
|
+
).optional()
|
|
1756
|
+
});
|
|
1757
|
+
const WaitForReplySchema = z.object({
|
|
1758
|
+
chatIdentifier: nonEmptyString("Phone number, email, or chat ID to monitor").optional(),
|
|
1759
|
+
threadSlug: nonEmptyString("Thread slug from list_conversations to monitor").optional(),
|
|
1760
|
+
timeoutSeconds: z.number().min(10).max(3600).default(300),
|
|
1761
|
+
pollIntervalSeconds: z.number().min(5).max(60).default(10),
|
|
1762
|
+
afterMessageId: z.number().int().positive().optional()
|
|
1763
|
+
});
|
|
1764
|
+
const WaitForReplyOutputSchema = z.object({
|
|
1765
|
+
received: z.boolean().optional(),
|
|
1766
|
+
messages: z.array(MessageSchema).optional(),
|
|
1767
|
+
count: z.number().int().optional(),
|
|
1768
|
+
timedOut: z.boolean().optional(),
|
|
1769
|
+
timeoutSeconds: z.number().optional(),
|
|
1770
|
+
threadSlug: z.string().optional(),
|
|
1771
|
+
chatIdentifier: z.string().optional(),
|
|
1772
|
+
cancelled: z.boolean().optional(),
|
|
1773
|
+
elapsedSeconds: z.number().optional()
|
|
1774
|
+
});
|
|
1775
|
+
const ListConversationsSchema = z.object({
|
|
1776
|
+
limit: z.number().int().min(0).default(20)
|
|
1777
|
+
});
|
|
1778
|
+
const ListConversationsOutputSchema = z.object({
|
|
1779
|
+
conversations: z.array(ConversationSchema),
|
|
1780
|
+
count: z.number().int(),
|
|
1781
|
+
hasMore: z.boolean(),
|
|
1782
|
+
nextOffset: z.number().int().nullable()
|
|
1783
|
+
});
|
|
1784
|
+
const SearchMessagesSchema = z.object({
|
|
1785
|
+
query: nonEmptyString("Search query"),
|
|
1786
|
+
limit: z.number().int().min(0).default(20),
|
|
1787
|
+
mode: z.enum(["literal", "fuzzy"]).default("fuzzy").describe(
|
|
1788
|
+
"Search mode. 'literal' = SQL LIKE substring match. 'fuzzy' = token-based scoring with typo tolerance; ranks by WRatio-style score."
|
|
1789
|
+
),
|
|
1790
|
+
minScore: z.number().min(0).max(1).default(0.6).describe(
|
|
1791
|
+
"Fuzzy mode: minimum normalized score (0-1) for a match to be returned. Ignored in literal mode."
|
|
1792
|
+
)
|
|
1793
|
+
});
|
|
1794
|
+
const SearchMessagesOutputSchema = z.object({
|
|
1795
|
+
query: z.string().optional(),
|
|
1796
|
+
mode: z.enum(["literal", "fuzzy"]).optional(),
|
|
1797
|
+
messages: z.array(MessageSchema),
|
|
1798
|
+
count: z.number().int(),
|
|
1799
|
+
hasMore: z.boolean(),
|
|
1800
|
+
nextOffset: z.number().int().nullable(),
|
|
1801
|
+
softCapWarning: z.string().optional()
|
|
1802
|
+
});
|
|
1803
|
+
const GetLogsSchema = z.object({
|
|
1804
|
+
tail: z.number().int().min(1).max(500).optional(),
|
|
1805
|
+
source: z.enum(["memory", "file", "all"]).optional()
|
|
1806
|
+
});
|
|
1807
|
+
const GetLogsOutputSchema = z.object({
|
|
1808
|
+
source: z.string(),
|
|
1809
|
+
tail: z.number().int(),
|
|
1810
|
+
sections: z.array(z.string())
|
|
1811
|
+
});
|
|
1812
|
+
z.object({});
|
|
1813
|
+
const RunBuildOutputSchema = z.object({
|
|
1814
|
+
ok: z.boolean(),
|
|
1815
|
+
stdout: z.string(),
|
|
1816
|
+
stderr: z.string()
|
|
1817
|
+
});
|
|
1818
|
+
z.object({});
|
|
1819
|
+
const RequestRestartOutputSchema = z.object({
|
|
1820
|
+
restartRequested: z.boolean()
|
|
1821
|
+
});
|
|
1822
|
+
z.object({});
|
|
1823
|
+
const HealthCheckOutputSchema = z.object({
|
|
1824
|
+
status: z.string(),
|
|
1825
|
+
issues: z.array(z.string()),
|
|
1826
|
+
uptimeMs: z.number(),
|
|
1827
|
+
idleMs: z.number(),
|
|
1828
|
+
pid: z.number(),
|
|
1829
|
+
node: z.string(),
|
|
1830
|
+
heapMb: z.number(),
|
|
1831
|
+
rssMb: z.number(),
|
|
1832
|
+
eventLoopP99Ms: z.number(),
|
|
1833
|
+
eventLoopMaxMs: z.number(),
|
|
1834
|
+
toolCallCount: z.number(),
|
|
1835
|
+
recentErrorCount: z.number(),
|
|
1836
|
+
engine: z.string()
|
|
1837
|
+
});
|
|
1838
|
+
z.object({});
|
|
1839
|
+
const GetLastSendErrorOutputSchema = z.object({
|
|
1840
|
+
lastSendError: z.object({
|
|
1841
|
+
message: z.string(),
|
|
1842
|
+
timestamp: z.string(),
|
|
1843
|
+
stderr: z.string().optional(),
|
|
1844
|
+
stdout: z.string().optional(),
|
|
1845
|
+
code: z.number().optional()
|
|
1846
|
+
}).nullable()
|
|
1847
|
+
});
|
|
1848
|
+
const ContactSchema = z.object({
|
|
1849
|
+
id: z.number().int(),
|
|
1850
|
+
displayName: z.string(),
|
|
1851
|
+
firstName: z.string().nullable(),
|
|
1852
|
+
lastName: z.string().nullable(),
|
|
1853
|
+
middleName: z.string().nullable(),
|
|
1854
|
+
nickname: z.string().nullable(),
|
|
1855
|
+
organization: z.string().nullable(),
|
|
1856
|
+
phoneNumbers: z.array(z.string()),
|
|
1857
|
+
emails: z.array(z.string())
|
|
1858
|
+
});
|
|
1859
|
+
const ListContactsSchema = z.object({
|
|
1860
|
+
limit: z.number().int().min(0).default(20).describe("Max contacts. 0 = unlimited (bounded by safety cap)."),
|
|
1861
|
+
offset: z.number().int().min(0).default(0).describe("Offset for pagination.")
|
|
1862
|
+
});
|
|
1863
|
+
const ListContactsOutputSchema = z.object({
|
|
1864
|
+
contacts: z.array(ContactSchema),
|
|
1865
|
+
count: z.number().int(),
|
|
1866
|
+
hasMore: z.boolean(),
|
|
1867
|
+
totalCount: z.number().int()
|
|
1868
|
+
});
|
|
1869
|
+
const SearchContactsSchema = z.object({
|
|
1870
|
+
query: nonEmptyString("Substring to match against name, phone, or email."),
|
|
1871
|
+
limit: z.number().int().min(0).default(20).describe("Max results. 0 = unlimited.")
|
|
1872
|
+
});
|
|
1873
|
+
const SearchContactsOutputSchema = z.object({
|
|
1874
|
+
query: z.string(),
|
|
1875
|
+
contacts: z.array(ContactSchema),
|
|
1876
|
+
count: z.number().int()
|
|
1877
|
+
});
|
|
1878
|
+
const GetContactSchema = z.object({
|
|
1879
|
+
handle: z.string().optional().describe("Phone number or email to look up."),
|
|
1880
|
+
id: z.number().int().optional().describe("Numeric contact id.")
|
|
1881
|
+
}).refine((v) => v.handle !== void 0 || v.id !== void 0, {
|
|
1882
|
+
message: "Provide either `handle` or `id`."
|
|
1883
|
+
});
|
|
1884
|
+
const GetContactOutputSchema = z.object({
|
|
1885
|
+
contact: ContactSchema.nullable()
|
|
1886
|
+
});
|
|
1887
|
+
const ResolveHandleSchema = z.object({
|
|
1888
|
+
handle: nonEmptyString("Phone number or email to resolve to a contact name.")
|
|
1889
|
+
});
|
|
1890
|
+
const ResolveHandleOutputSchema = z.object({
|
|
1891
|
+
handle: z.string(),
|
|
1892
|
+
displayName: z.string(),
|
|
1893
|
+
contactId: z.number().int().nullable(),
|
|
1894
|
+
label: z.string().nullable(),
|
|
1895
|
+
resolved: z.boolean()
|
|
1896
|
+
});
|
|
1897
|
+
const CheckImessageAvailabilitySchema = z.object({
|
|
1898
|
+
handle: nonEmptyString("Phone number or email to preflight-check for reachability.")
|
|
1899
|
+
});
|
|
1900
|
+
const CheckImessageAvailabilityOutputSchema = z.object({
|
|
1901
|
+
handle: z.string(),
|
|
1902
|
+
service: z.enum(["iMessage", "SMS", "unknown"]),
|
|
1903
|
+
reachable: z.boolean(),
|
|
1904
|
+
hint: z.string().optional()
|
|
1905
|
+
});
|
|
1906
|
+
const AttachmentRecordSchema = z.object({
|
|
1907
|
+
rowId: z.number().int(),
|
|
1908
|
+
filename: z.string(),
|
|
1909
|
+
mimeType: z.string().nullable(),
|
|
1910
|
+
transferName: z.string().nullable(),
|
|
1911
|
+
totalBytes: z.number().int(),
|
|
1912
|
+
createdDate: z.string(),
|
|
1913
|
+
chatId: z.string()
|
|
1914
|
+
});
|
|
1915
|
+
const SearchAttachmentsSchema = z.object({
|
|
1916
|
+
mimePrefix: z.string().optional().describe("Filter by MIME type prefix, e.g. 'image/', 'video/', 'application/pdf'."),
|
|
1917
|
+
chatIdentifier: z.string().optional().describe("Restrict to a single chat (use chat_identifier from list_conversations)."),
|
|
1918
|
+
since: z.string().optional().describe("ISO date or relative ('1 week ago'). Lower bound on attachment creation."),
|
|
1919
|
+
until: z.string().optional().describe("ISO date or relative. Upper bound on creation."),
|
|
1920
|
+
limit: z.number().int().min(0).default(20).describe("Max results. 0 = unlimited (capped at 1000).")
|
|
1921
|
+
});
|
|
1922
|
+
const SearchAttachmentsOutputSchema = z.object({
|
|
1923
|
+
attachments: z.array(AttachmentRecordSchema),
|
|
1924
|
+
count: z.number().int()
|
|
1925
|
+
});
|
|
1926
|
+
const ChatAnalyticsSchema = z.object({
|
|
1927
|
+
type: z.enum([
|
|
1928
|
+
"messaging_streaks",
|
|
1929
|
+
"double_texts",
|
|
1930
|
+
"response_time_stats",
|
|
1931
|
+
"daily_heatmap",
|
|
1932
|
+
"tapback_summary",
|
|
1933
|
+
"year_in_review_wrapped"
|
|
1934
|
+
]).describe(
|
|
1935
|
+
"Which analytic to compute. Six priority types are implemented; 20 more are reserved for future versions and return a structured 'not_yet_implemented' error."
|
|
1936
|
+
),
|
|
1937
|
+
windowDays: z.number().int().min(1).max(3650).default(90).describe("Days of history to analyze. Defaults to 90; year_in_review pin to 365 internally.")
|
|
1938
|
+
});
|
|
1939
|
+
const ChatAnalyticsOutputSchema = z.object({
|
|
1940
|
+
type: z.string(),
|
|
1941
|
+
windowDays: z.number().int(),
|
|
1942
|
+
computedAtIso: z.string(),
|
|
1943
|
+
fromCache: z.boolean(),
|
|
1944
|
+
data: z.unknown()
|
|
1945
|
+
});
|
|
1946
|
+
const GetAttachmentSchema = z.object({
|
|
1947
|
+
rowId: z.number().int().describe("Attachment ROWID (from search_attachments)."),
|
|
1948
|
+
inlineMaxBytes: z.number().int().default(5e6).describe("If file is ≤ this size, return base64 content inline; otherwise return path only.")
|
|
1949
|
+
});
|
|
1950
|
+
const GetAttachmentOutputSchema = z.object({
|
|
1951
|
+
rowId: z.number().int(),
|
|
1952
|
+
filename: z.string(),
|
|
1953
|
+
resolvedPath: z.string(),
|
|
1954
|
+
mimeType: z.string().nullable(),
|
|
1955
|
+
totalBytes: z.number().int(),
|
|
1956
|
+
inline: z.boolean(),
|
|
1957
|
+
base64: z.string().optional(),
|
|
1958
|
+
converted: z.string().optional().describe("Set when source was HEIC and we converted to PNG.")
|
|
1959
|
+
});
|
|
1960
|
+
const noArgsSchema = { type: "object", properties: {} };
|
|
1961
|
+
const annotations = {
|
|
1962
|
+
read: {
|
|
1963
|
+
readOnlyHint: true,
|
|
1964
|
+
destructiveHint: false,
|
|
1965
|
+
idempotentHint: true,
|
|
1966
|
+
openWorldHint: false
|
|
1967
|
+
},
|
|
1968
|
+
status: {
|
|
1969
|
+
readOnlyHint: true,
|
|
1970
|
+
destructiveHint: false,
|
|
1971
|
+
idempotentHint: true,
|
|
1972
|
+
openWorldHint: false
|
|
1973
|
+
},
|
|
1974
|
+
send: {
|
|
1975
|
+
readOnlyHint: false,
|
|
1976
|
+
destructiveHint: false,
|
|
1977
|
+
idempotentHint: false,
|
|
1978
|
+
openWorldHint: true
|
|
1979
|
+
},
|
|
1980
|
+
export: {
|
|
1981
|
+
readOnlyHint: false,
|
|
1982
|
+
destructiveHint: true,
|
|
1983
|
+
idempotentHint: false,
|
|
1984
|
+
openWorldHint: false
|
|
1985
|
+
},
|
|
1986
|
+
build: {
|
|
1987
|
+
readOnlyHint: false,
|
|
1988
|
+
destructiveHint: false,
|
|
1989
|
+
idempotentHint: false,
|
|
1990
|
+
openWorldHint: true
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1993
|
+
const TOOL_TIMEOUTS_MS = {
|
|
1994
|
+
wait_for_reply: 0,
|
|
1995
|
+
run_build: 12e4,
|
|
1996
|
+
search_messages: 6e4,
|
|
1997
|
+
get_messages: 6e4,
|
|
1998
|
+
list_conversations: 6e4,
|
|
1999
|
+
get_unread_messages: 6e4,
|
|
2000
|
+
send_message: 6e4,
|
|
2001
|
+
health_check: 5e3,
|
|
2002
|
+
export_messages: 6e5,
|
|
2003
|
+
get_logs: 1e4,
|
|
2004
|
+
get_last_send_error: 5e3,
|
|
2005
|
+
request_restart: 5e3,
|
|
2006
|
+
list_contacts: 1e4,
|
|
2007
|
+
search_contacts: 1e4,
|
|
2008
|
+
get_contact: 5e3,
|
|
2009
|
+
resolve_handle: 5e3,
|
|
2010
|
+
check_imessage_availability: 1e4,
|
|
2011
|
+
search_attachments: 3e4,
|
|
2012
|
+
get_attachment: 3e4,
|
|
2013
|
+
chat_analytics: 6e4
|
|
2014
|
+
};
|
|
2015
|
+
function resolveLimit(limit, defaultValue = 20) {
|
|
2016
|
+
if (limit === void 0) return defaultValue;
|
|
2017
|
+
if (limit === 0) return UNLIMITED;
|
|
2018
|
+
return limit;
|
|
2019
|
+
}
|
|
2020
|
+
const TOOLS = [
|
|
2021
|
+
{
|
|
2022
|
+
name: "get_messages",
|
|
2023
|
+
description: "Get recent iMessages. Optionally filter by conversation. Response footer includes oldestMessageId for beforeMessageId pagination; use export_messages for very large histories.",
|
|
2024
|
+
annotations: annotations.read,
|
|
2025
|
+
inputSchema: {
|
|
2026
|
+
type: "object",
|
|
2027
|
+
properties: {
|
|
2028
|
+
limit: { type: "number", default: 20, description: "Number of messages. 0 = unlimited." },
|
|
2029
|
+
chatIdentifier: { type: "string", description: "Phone number, email, or chat ID" },
|
|
2030
|
+
threadSlug: { type: "string", description: "Thread slug from list_conversations" },
|
|
2031
|
+
beforeMessageId: {
|
|
2032
|
+
type: "number",
|
|
2033
|
+
description: "Fetch messages older than this message id"
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
},
|
|
2037
|
+
// @ts-expect-error
|
|
2038
|
+
outputSchema: zodToJsonSchema(GetMessagesOutputSchema)
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
name: "export_messages",
|
|
2042
|
+
description: "Stream-export a conversation to markdown, csv, json, or ndjson without loading all history into memory.",
|
|
2043
|
+
annotations: annotations.export,
|
|
2044
|
+
inputSchema: {
|
|
2045
|
+
type: "object",
|
|
2046
|
+
required: ["outputPath"],
|
|
2047
|
+
properties: {
|
|
2048
|
+
chatIdentifier: { type: "string", description: "Phone number, email, or chat ID" },
|
|
2049
|
+
threadSlug: { type: "string", description: "Thread slug from list_conversations" },
|
|
2050
|
+
format: {
|
|
2051
|
+
type: "string",
|
|
2052
|
+
enum: ["markdown", "csv", "json", "ndjson"],
|
|
2053
|
+
default: "markdown"
|
|
2054
|
+
},
|
|
2055
|
+
outputPath: {
|
|
2056
|
+
type: "string",
|
|
2057
|
+
description: "Absolute output path. Existing files are overwritten."
|
|
2058
|
+
},
|
|
2059
|
+
since: { type: "string", description: "Earliest date, ISO or relative" },
|
|
2060
|
+
until: { type: "string", description: "Latest date, ISO or relative" },
|
|
2061
|
+
pageSize: { type: "number", default: 1e3, description: "Internal page size, 100-5000" }
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2064
|
+
// @ts-expect-error
|
|
2065
|
+
outputSchema: zodToJsonSchema(ExportMessagesOutputSchema)
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
name: "get_unread_messages",
|
|
2069
|
+
description: "Get unread iMessages across all conversations, newest first.",
|
|
2070
|
+
annotations: annotations.read,
|
|
2071
|
+
inputSchema: {
|
|
2072
|
+
type: "object",
|
|
2073
|
+
properties: {
|
|
2074
|
+
limit: { type: "number", description: "Max unread messages. 0 = unlimited. Default 100." }
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
// @ts-expect-error
|
|
2078
|
+
outputSchema: zodToJsonSchema(GetUnreadMessagesOutputSchema)
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
name: "send_message",
|
|
2082
|
+
description: "Send an iMessage or SMS. Use recipient for 1-on-1 or threadSlug for existing threads, including groups. Optional `attachments` is an array of absolute file paths sent as follow-up messages (1-on-1 only — Messages.app does not reliably accept file sends to group chats).",
|
|
2083
|
+
annotations: annotations.send,
|
|
2084
|
+
inputSchema: {
|
|
2085
|
+
type: "object",
|
|
2086
|
+
required: ["message"],
|
|
2087
|
+
properties: {
|
|
2088
|
+
recipient: { type: "string", description: "Phone number or email" },
|
|
2089
|
+
threadSlug: { type: "string", description: "Thread slug from list_conversations" },
|
|
2090
|
+
message: { type: "string", description: "Message text to send" },
|
|
2091
|
+
attachments: {
|
|
2092
|
+
type: "array",
|
|
2093
|
+
items: { type: "string" },
|
|
2094
|
+
description: "Optional absolute file paths to send after the text. 1-on-1 only."
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
},
|
|
2098
|
+
// @ts-expect-error
|
|
2099
|
+
outputSchema: zodToJsonSchema(SendMessageOutputSchema)
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
name: "wait_for_reply",
|
|
2103
|
+
description: "Wait for a new incoming message in a conversation until timeout or client cancellation.",
|
|
2104
|
+
annotations: annotations.read,
|
|
2105
|
+
inputSchema: {
|
|
2106
|
+
type: "object",
|
|
2107
|
+
properties: {
|
|
2108
|
+
chatIdentifier: { type: "string", description: "Phone number, email, or chat ID" },
|
|
2109
|
+
threadSlug: { type: "string", description: "Thread slug from list_conversations" },
|
|
2110
|
+
timeoutSeconds: {
|
|
2111
|
+
type: "number",
|
|
2112
|
+
default: 300,
|
|
2113
|
+
description: "Timeout in seconds, 10-3600"
|
|
2114
|
+
},
|
|
2115
|
+
pollIntervalSeconds: {
|
|
2116
|
+
type: "number",
|
|
2117
|
+
default: 10,
|
|
2118
|
+
description: "Polling interval in seconds, 5-60"
|
|
2119
|
+
},
|
|
2120
|
+
afterMessageId: { type: "number", description: "Only return messages after this id" }
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
// @ts-expect-error
|
|
2124
|
+
outputSchema: zodToJsonSchema(WaitForReplyOutputSchema)
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
name: "list_conversations",
|
|
2128
|
+
description: "List recent conversations with thread slugs, snippets, unread counts, participants, and service metadata.",
|
|
2129
|
+
annotations: annotations.read,
|
|
2130
|
+
inputSchema: {
|
|
2131
|
+
type: "object",
|
|
2132
|
+
properties: {
|
|
2133
|
+
limit: {
|
|
2134
|
+
type: "number",
|
|
2135
|
+
default: 20,
|
|
2136
|
+
description: "Number of conversations. 0 = unlimited."
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
// @ts-expect-error
|
|
2141
|
+
outputSchema: zodToJsonSchema(ListConversationsOutputSchema)
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
name: "search_messages",
|
|
2145
|
+
description: "Search message text across all conversations.",
|
|
2146
|
+
annotations: annotations.read,
|
|
2147
|
+
inputSchema: {
|
|
2148
|
+
type: "object",
|
|
2149
|
+
required: ["query"],
|
|
2150
|
+
properties: {
|
|
2151
|
+
query: { type: "string", description: "Search query" },
|
|
2152
|
+
limit: { type: "number", default: 20, description: "Number of results. 0 = unlimited." }
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
// @ts-expect-error
|
|
2156
|
+
outputSchema: zodToJsonSchema(SearchMessagesOutputSchema)
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
name: "get_logs",
|
|
2160
|
+
description: "Return debug logs from memory, file, or both.",
|
|
2161
|
+
annotations: annotations.status,
|
|
2162
|
+
inputSchema: {
|
|
2163
|
+
type: "object",
|
|
2164
|
+
properties: {
|
|
2165
|
+
tail: { type: "number", description: "Return last N lines. Default 50." },
|
|
2166
|
+
source: { type: "string", enum: ["memory", "file", "all"], description: "Log source" }
|
|
2167
|
+
}
|
|
2168
|
+
},
|
|
2169
|
+
// @ts-expect-error
|
|
2170
|
+
outputSchema: zodToJsonSchema(GetLogsOutputSchema)
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
name: "get_last_send_error",
|
|
2174
|
+
description: "Return details for the last send_message failure.",
|
|
2175
|
+
annotations: annotations.status,
|
|
2176
|
+
inputSchema: noArgsSchema,
|
|
2177
|
+
// @ts-expect-error
|
|
2178
|
+
outputSchema: zodToJsonSchema(GetLastSendErrorOutputSchema)
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
name: "run_build",
|
|
2182
|
+
description: "Run pnpm build in the project directory and return stdout/stderr.",
|
|
2183
|
+
annotations: annotations.build,
|
|
2184
|
+
inputSchema: noArgsSchema,
|
|
2185
|
+
// @ts-expect-error
|
|
2186
|
+
outputSchema: zodToJsonSchema(RunBuildOutputSchema)
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
name: "request_restart",
|
|
2190
|
+
description: "Exit the MCP server process so the client can restart it and load new code.",
|
|
2191
|
+
annotations: annotations.build,
|
|
2192
|
+
inputSchema: noArgsSchema,
|
|
2193
|
+
// @ts-expect-error
|
|
2194
|
+
outputSchema: zodToJsonSchema(RequestRestartOutputSchema)
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
name: "health_check",
|
|
2198
|
+
description: "Return in-memory MCP vital signs without touching SQLite.",
|
|
2199
|
+
annotations: annotations.status,
|
|
2200
|
+
inputSchema: noArgsSchema,
|
|
2201
|
+
// @ts-expect-error
|
|
2202
|
+
outputSchema: zodToJsonSchema(HealthCheckOutputSchema)
|
|
2203
|
+
},
|
|
2204
|
+
{
|
|
2205
|
+
name: "list_contacts",
|
|
2206
|
+
description: "List loaded contacts (from macOS Address Book + iCloud sources), sorted by name. Use search_contacts for substring matching.",
|
|
2207
|
+
annotations: annotations.read,
|
|
2208
|
+
inputSchema: {
|
|
2209
|
+
type: "object",
|
|
2210
|
+
properties: {
|
|
2211
|
+
limit: { type: "number", default: 20, description: "Max contacts. 0 = unlimited." },
|
|
2212
|
+
offset: { type: "number", default: 0, description: "Offset for pagination." }
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
// @ts-expect-error
|
|
2216
|
+
outputSchema: zodToJsonSchema(ListContactsOutputSchema)
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
name: "search_contacts",
|
|
2220
|
+
description: "Substring-match contacts by display name, phone number, or email.",
|
|
2221
|
+
annotations: annotations.read,
|
|
2222
|
+
inputSchema: {
|
|
2223
|
+
type: "object",
|
|
2224
|
+
required: ["query"],
|
|
2225
|
+
properties: {
|
|
2226
|
+
query: { type: "string", description: "Substring (case-insensitive for name/email)." },
|
|
2227
|
+
limit: { type: "number", default: 20, description: "Max results. 0 = unlimited." }
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
2230
|
+
// @ts-expect-error
|
|
2231
|
+
outputSchema: zodToJsonSchema(SearchContactsOutputSchema)
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
name: "get_contact",
|
|
2235
|
+
description: "Fetch a single contact by handle (phone/email) or by numeric id. Returns null if not found.",
|
|
2236
|
+
annotations: annotations.read,
|
|
2237
|
+
inputSchema: {
|
|
2238
|
+
type: "object",
|
|
2239
|
+
properties: {
|
|
2240
|
+
handle: { type: "string", description: "Phone number or email to look up." },
|
|
2241
|
+
id: { type: "number", description: "Numeric contact id." }
|
|
2242
|
+
}
|
|
2243
|
+
},
|
|
2244
|
+
// @ts-expect-error
|
|
2245
|
+
outputSchema: zodToJsonSchema(GetContactOutputSchema)
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
name: "resolve_handle",
|
|
2249
|
+
description: "Resolve a phone number or email to its contact display name. Pass-through if unknown.",
|
|
2250
|
+
annotations: annotations.read,
|
|
2251
|
+
inputSchema: {
|
|
2252
|
+
type: "object",
|
|
2253
|
+
required: ["handle"],
|
|
2254
|
+
properties: {
|
|
2255
|
+
handle: { type: "string", description: "Phone number or email." }
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
// @ts-expect-error
|
|
2259
|
+
outputSchema: zodToJsonSchema(ResolveHandleOutputSchema)
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
name: "check_imessage_availability",
|
|
2263
|
+
description: "Preflight check: is this handle reachable via iMessage or SMS? Call BEFORE send_message to avoid wasted send attempts to unreachable recipients. Returns the best-guess service plus a human-readable hint when unreachable.",
|
|
2264
|
+
annotations: annotations.status,
|
|
2265
|
+
inputSchema: {
|
|
2266
|
+
type: "object",
|
|
2267
|
+
required: ["handle"],
|
|
2268
|
+
properties: {
|
|
2269
|
+
handle: {
|
|
2270
|
+
type: "string",
|
|
2271
|
+
description: "Phone number (E.164 preferred) or email to preflight."
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
// @ts-expect-error
|
|
2276
|
+
outputSchema: zodToJsonSchema(CheckImessageAvailabilityOutputSchema)
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
name: "search_attachments",
|
|
2280
|
+
description: "Search attachments (images, videos, files) by MIME type prefix, date range, and/or chat. Returns metadata only — use get_attachment to fetch bytes. Excludes stickers and Apple plugin payloads.",
|
|
2281
|
+
annotations: annotations.read,
|
|
2282
|
+
inputSchema: {
|
|
2283
|
+
type: "object",
|
|
2284
|
+
properties: {
|
|
2285
|
+
mimePrefix: { type: "string", description: "e.g. 'image/', 'video/', 'application/pdf'." },
|
|
2286
|
+
chatIdentifier: { type: "string", description: "Restrict to one chat." },
|
|
2287
|
+
since: { type: "string", description: "ISO date or relative ('1 week ago')." },
|
|
2288
|
+
until: { type: "string", description: "ISO date or relative." },
|
|
2289
|
+
limit: {
|
|
2290
|
+
type: "number",
|
|
2291
|
+
default: 20,
|
|
2292
|
+
description: "Max results. 0 = unlimited (cap 1000)."
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
},
|
|
2296
|
+
// @ts-expect-error
|
|
2297
|
+
outputSchema: zodToJsonSchema(SearchAttachmentsOutputSchema)
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
name: "get_attachment",
|
|
2301
|
+
description: "Fetch an attachment by ROWID (from search_attachments). Returns base64 inline if ≤ inlineMaxBytes (default 5MB); otherwise returns the resolved path only. HEIC images are auto-converted to PNG via macOS sips.",
|
|
2302
|
+
annotations: annotations.read,
|
|
2303
|
+
inputSchema: {
|
|
2304
|
+
type: "object",
|
|
2305
|
+
required: ["rowId"],
|
|
2306
|
+
properties: {
|
|
2307
|
+
rowId: { type: "number", description: "Attachment ROWID from search_attachments." },
|
|
2308
|
+
inlineMaxBytes: { type: "number", default: 5e6, description: "Inline byte cap." }
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
// @ts-expect-error
|
|
2312
|
+
outputSchema: zodToJsonSchema(GetAttachmentOutputSchema)
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
name: "chat_analytics",
|
|
2316
|
+
description: "Compute analytics over your chat history. Pick a `type`: messaging_streaks, double_texts, response_time_stats, daily_heatmap, tapback_summary, or year_in_review_wrapped. Results are cached at ~/.imsg-mcp/analytics-cache.db keyed on (type, args, MAX(message.rowid)) so subsequent calls without new messages hit cache. 20 additional analytic types are reserved for future versions.",
|
|
2317
|
+
annotations: annotations.read,
|
|
2318
|
+
inputSchema: {
|
|
2319
|
+
type: "object",
|
|
2320
|
+
required: ["type"],
|
|
2321
|
+
properties: {
|
|
2322
|
+
type: {
|
|
2323
|
+
type: "string",
|
|
2324
|
+
enum: [
|
|
2325
|
+
"messaging_streaks",
|
|
2326
|
+
"double_texts",
|
|
2327
|
+
"response_time_stats",
|
|
2328
|
+
"daily_heatmap",
|
|
2329
|
+
"tapback_summary",
|
|
2330
|
+
"year_in_review_wrapped"
|
|
2331
|
+
]
|
|
2332
|
+
},
|
|
2333
|
+
windowDays: {
|
|
2334
|
+
type: "number",
|
|
2335
|
+
default: 90,
|
|
2336
|
+
description: "Days of history to scan (1-3650). year_in_review pins to 365."
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
},
|
|
2340
|
+
// @ts-expect-error
|
|
2341
|
+
outputSchema: zodToJsonSchema(ChatAnalyticsOutputSchema)
|
|
2342
|
+
}
|
|
2343
|
+
];
|
|
2344
|
+
const DEV_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
2345
|
+
"health_check",
|
|
2346
|
+
"get_logs",
|
|
2347
|
+
"get_last_send_error",
|
|
2348
|
+
"run_build",
|
|
2349
|
+
"request_restart"
|
|
2350
|
+
]);
|
|
2351
|
+
function isDevMode() {
|
|
2352
|
+
return process.env.IMSG_DEV === "1";
|
|
2353
|
+
}
|
|
2354
|
+
function getActiveTools() {
|
|
2355
|
+
if (isDevMode()) return TOOLS;
|
|
2356
|
+
return TOOLS.filter((t) => !DEV_TOOL_NAMES.has(t.name));
|
|
2357
|
+
}
|
|
2358
|
+
const INSTRUCTIONS_UUID = randomUUID();
|
|
2359
|
+
function wrapUntrusted(text) {
|
|
2360
|
+
if (text == null || text === "") return "";
|
|
2361
|
+
const neutralized = text.replace(/<\/untrusted>/gi, "</untrusted>").replace(new RegExp(`<\\/instructions uuid="${INSTRUCTIONS_UUID}">`, "gi"), "").replace(/<\/instructions>/gi, "</instructions>");
|
|
2362
|
+
return `<untrusted>${neutralized}</untrusted>`;
|
|
2363
|
+
}
|
|
2364
|
+
const ANSI_REGEX = new RegExp(
|
|
2365
|
+
[
|
|
2366
|
+
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
2367
|
+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))"
|
|
2368
|
+
].join("|"),
|
|
2369
|
+
"g"
|
|
2370
|
+
);
|
|
2371
|
+
const CONTROL_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
|
|
2372
|
+
function sanitizeUserText(text, maxLength = 4096) {
|
|
2373
|
+
if (text == null) return null;
|
|
2374
|
+
let sanitized = text.replace(ANSI_REGEX, "");
|
|
2375
|
+
sanitized = sanitized.replace(CONTROL_CHAR_REGEX, "�");
|
|
2376
|
+
if (sanitized.length > maxLength) {
|
|
2377
|
+
sanitized = `${sanitized.slice(0, maxLength - 1)}…`;
|
|
2378
|
+
}
|
|
2379
|
+
return sanitized;
|
|
2380
|
+
}
|
|
2381
|
+
const execFileAsync = promisify(execFile);
|
|
2382
|
+
class ToolTimeoutError extends Error {
|
|
2383
|
+
constructor(toolName, timeoutMs) {
|
|
2384
|
+
super(
|
|
2385
|
+
`Tool '${toolName}' timed out after ${timeoutMs}ms. The MCP server has unblocked; the underlying query may still be running in the background.`
|
|
2386
|
+
);
|
|
2387
|
+
this.toolName = toolName;
|
|
2388
|
+
this.timeoutMs = timeoutMs;
|
|
2389
|
+
this.name = "ToolTimeoutError";
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
function withTimeout(toolName, fn) {
|
|
2393
|
+
const ms = TOOL_TIMEOUTS_MS[toolName] ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
2394
|
+
if (ms <= 0) return fn();
|
|
2395
|
+
return new Promise((resolve, reject) => {
|
|
2396
|
+
const timer = setTimeout(() => reject(new ToolTimeoutError(toolName, ms)), ms);
|
|
2397
|
+
timer.unref();
|
|
2398
|
+
fn().then(
|
|
2399
|
+
(value) => {
|
|
2400
|
+
clearTimeout(timer);
|
|
2401
|
+
resolve(value);
|
|
2402
|
+
},
|
|
2403
|
+
(error) => {
|
|
2404
|
+
clearTimeout(timer);
|
|
2405
|
+
reject(error);
|
|
2406
|
+
}
|
|
2407
|
+
);
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
function round1(n) {
|
|
2411
|
+
return Math.round(n * 10) / 10;
|
|
2412
|
+
}
|
|
2413
|
+
function formatDuration(ms) {
|
|
2414
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2415
|
+
const sec = Math.floor(ms / 1e3);
|
|
2416
|
+
if (sec < 60) return `${sec}s`;
|
|
2417
|
+
const min = Math.floor(sec / 60);
|
|
2418
|
+
if (min < 60) return `${min}m ${sec % 60}s`;
|
|
2419
|
+
const hr = Math.floor(min / 60);
|
|
2420
|
+
return `${hr}h ${min % 60}m`;
|
|
2421
|
+
}
|
|
2422
|
+
function relativeDate(d) {
|
|
2423
|
+
const now = /* @__PURE__ */ new Date();
|
|
2424
|
+
const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
|
|
2425
|
+
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
|
|
2426
|
+
if (sameDay) return `Today ${time}`;
|
|
2427
|
+
const yesterday = new Date(now);
|
|
2428
|
+
yesterday.setDate(now.getDate() - 1);
|
|
2429
|
+
if (d.getFullYear() === yesterday.getFullYear() && d.getMonth() === yesterday.getMonth() && d.getDate() === yesterday.getDate()) {
|
|
2430
|
+
return `Yesterday ${time}`;
|
|
2431
|
+
}
|
|
2432
|
+
return `${d.getMonth() + 1}/${d.getDate()} ${time}`;
|
|
2433
|
+
}
|
|
2434
|
+
function formatMessage(msg, conversationLabel) {
|
|
2435
|
+
const direction = msg.isFromMe ? "→" : "←";
|
|
2436
|
+
const dateStr = relativeDate(msg.date);
|
|
2437
|
+
const svcTag = msg.service === "SMS" ? " [SMS]" : "";
|
|
2438
|
+
let sender;
|
|
2439
|
+
if (msg.isFromMe) {
|
|
2440
|
+
sender = "me";
|
|
2441
|
+
} else if (msg.displayName && msg.displayName !== msg.handle) {
|
|
2442
|
+
sender = `${msg.displayName} (${msg.handle})`;
|
|
2443
|
+
} else {
|
|
2444
|
+
sender = msg.handle;
|
|
2445
|
+
}
|
|
2446
|
+
let status = "";
|
|
2447
|
+
if (!msg.isFromMe && !msg.isRead) {
|
|
2448
|
+
status = " [UNREAD]";
|
|
2449
|
+
} else if (msg.isFromMe) {
|
|
2450
|
+
if (msg.dateRead) {
|
|
2451
|
+
status = ` [Read ${msg.dateRead.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true })}]`;
|
|
2452
|
+
} else if (msg.isDelivered) {
|
|
2453
|
+
status = " [Delivered]";
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const convCtx = conversationLabel ? ` {${conversationLabel}}` : "";
|
|
2457
|
+
const rawText = sanitizeUserText(msg.text);
|
|
2458
|
+
const text = rawText ? wrapUntrusted(rawText) : "(no text)";
|
|
2459
|
+
return `[${dateStr}] ${direction} ${sender}${svcTag}: ${text}${status}${convCtx}`;
|
|
2460
|
+
}
|
|
2461
|
+
function messageToStructured(msg) {
|
|
2462
|
+
return {
|
|
2463
|
+
...msg,
|
|
2464
|
+
text: sanitizeUserText(msg.text),
|
|
2465
|
+
date: msg.date.toISOString(),
|
|
2466
|
+
dateRead: msg.dateRead?.toISOString() ?? null,
|
|
2467
|
+
dateDelivered: msg.dateDelivered?.toISOString() ?? null
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function sleep(ms) {
|
|
2471
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2472
|
+
}
|
|
2473
|
+
function toolText(text, structuredContent) {
|
|
2474
|
+
return {
|
|
2475
|
+
content: [{ type: "text", text }],
|
|
2476
|
+
...structuredContent ? { structuredContent } : {}
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
function toolError(text, _structuredContent) {
|
|
2480
|
+
return {
|
|
2481
|
+
...toolText(text),
|
|
2482
|
+
isError: true
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
function validateExportOutputPath(outputPath) {
|
|
2486
|
+
if (!isAbsolute(outputPath)) {
|
|
2487
|
+
return "outputPath must be an absolute path.";
|
|
2488
|
+
}
|
|
2489
|
+
const parent = dirname(outputPath);
|
|
2490
|
+
if (!existsSync(parent)) {
|
|
2491
|
+
return `Parent directory does not exist: ${parent}`;
|
|
2492
|
+
}
|
|
2493
|
+
const parentStat = statSync(parent);
|
|
2494
|
+
if (!parentStat.isDirectory()) {
|
|
2495
|
+
return `Parent path is not a directory: ${parent}`;
|
|
2496
|
+
}
|
|
2497
|
+
if (existsSync(outputPath) && statSync(outputPath).isDirectory()) {
|
|
2498
|
+
return `outputPath points to a directory, not a file: ${outputPath}`;
|
|
2499
|
+
}
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
function engineLabel() {
|
|
2503
|
+
return hasNativeModule() ? "Rust parser + TS DB" : "TS";
|
|
2504
|
+
}
|
|
2505
|
+
class IMessageMCPServer {
|
|
2506
|
+
server;
|
|
2507
|
+
db;
|
|
2508
|
+
// Activity tracking — surfaced via health_check and watchdog
|
|
2509
|
+
toolCallCount = 0;
|
|
2510
|
+
recentErrorCount = 0;
|
|
2511
|
+
lastActivityTs = Date.now();
|
|
2512
|
+
constructor() {
|
|
2513
|
+
this.server = new Server(
|
|
2514
|
+
{
|
|
2515
|
+
name: APP_NAME,
|
|
2516
|
+
version: APP_VERSION
|
|
2517
|
+
},
|
|
2518
|
+
{
|
|
2519
|
+
capabilities: {
|
|
2520
|
+
tools: {},
|
|
2521
|
+
resources: { subscribe: false, listChanged: false }
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
);
|
|
2525
|
+
this.db = new IMessageDB(getImsgDbPath(), getContactsDbPaths(), getSlugsDbPath());
|
|
2526
|
+
this.setupHandlers();
|
|
2527
|
+
}
|
|
2528
|
+
setupHandlers() {
|
|
2529
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2530
|
+
tools: getActiveTools()
|
|
2531
|
+
}));
|
|
2532
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2533
|
+
const { name, arguments: args } = request.params;
|
|
2534
|
+
this.lastActivityTs = Date.now();
|
|
2535
|
+
this.toolCallCount++;
|
|
2536
|
+
noteActivity();
|
|
2537
|
+
const signal = extra?.signal;
|
|
2538
|
+
const startedAt = performance.now();
|
|
2539
|
+
const stampMeta = (result) => {
|
|
2540
|
+
const duration_ms = Math.round((performance.now() - startedAt) * 10) / 10;
|
|
2541
|
+
return { ...result, _meta: { engine: engineLabel(), duration_ms } };
|
|
2542
|
+
};
|
|
2543
|
+
try {
|
|
2544
|
+
return stampMeta(await withTimeout(name, () => this.dispatchTool(name, args, signal)));
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
const isTimeout = error instanceof ToolTimeoutError;
|
|
2547
|
+
appendLog(isTimeout ? "warn" : "error", isTimeout ? "Tool timed out" : "Tool error", {
|
|
2548
|
+
tool: name,
|
|
2549
|
+
error: error.message || String(error)
|
|
2550
|
+
});
|
|
2551
|
+
if (!isTimeout) this.recentErrorCount++;
|
|
2552
|
+
return stampMeta(
|
|
2553
|
+
toolError(`Error: ${error.message || String(error)}`, {
|
|
2554
|
+
error: error.message || String(error)
|
|
2555
|
+
})
|
|
2556
|
+
);
|
|
2557
|
+
} finally {
|
|
2558
|
+
this.db.scheduleBackgroundRefresh();
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
2561
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
2562
|
+
resources: []
|
|
2563
|
+
}));
|
|
2564
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
2565
|
+
resourceTemplates: [
|
|
2566
|
+
{
|
|
2567
|
+
uriTemplate: "messages://recent/{hours}",
|
|
2568
|
+
name: "Recent messages (last N hours)",
|
|
2569
|
+
description: "Read all messages across every chat from the last {hours} hours.",
|
|
2570
|
+
mimeType: "application/json"
|
|
2571
|
+
},
|
|
2572
|
+
{
|
|
2573
|
+
uriTemplate: "messages://contact/{handle}/{hours}",
|
|
2574
|
+
name: "Messages with a contact (last N hours)",
|
|
2575
|
+
description: "Read messages from the chat containing {handle} (phone/email) in the last {hours} hours.",
|
|
2576
|
+
mimeType: "application/json"
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
uriTemplate: "contacts://",
|
|
2580
|
+
name: "All loaded contacts",
|
|
2581
|
+
description: "Read all contacts from the macOS Address Book sources.",
|
|
2582
|
+
mimeType: "application/json"
|
|
2583
|
+
}
|
|
2584
|
+
]
|
|
2585
|
+
}));
|
|
2586
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2587
|
+
const { uri } = request.params;
|
|
2588
|
+
try {
|
|
2589
|
+
const result = await this.readResource(uri);
|
|
2590
|
+
return {
|
|
2591
|
+
contents: [
|
|
2592
|
+
{
|
|
2593
|
+
uri,
|
|
2594
|
+
mimeType: "application/json",
|
|
2595
|
+
text: JSON.stringify(result)
|
|
2596
|
+
}
|
|
2597
|
+
]
|
|
2598
|
+
};
|
|
2599
|
+
} catch (e) {
|
|
2600
|
+
throw new Error(`Resource ${uri} failed: ${e.message ?? e}`);
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
async readResource(uri) {
|
|
2605
|
+
let m = uri.match(/^messages:\/\/recent\/(\d+)$/);
|
|
2606
|
+
if (m?.[1]) {
|
|
2607
|
+
const hours = Number(m[1]);
|
|
2608
|
+
const cutoffMs = Date.now() - hours * 3600 * 1e3;
|
|
2609
|
+
const msgs = await this.db.getRecentMessages(500);
|
|
2610
|
+
return {
|
|
2611
|
+
windowHours: hours,
|
|
2612
|
+
messages: msgs.filter((mm) => mm.date.getTime() >= cutoffMs).map(messageToStructured)
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
m = uri.match(/^messages:\/\/contact\/([^/]+)\/(\d+)$/);
|
|
2616
|
+
if (m?.[1] && m?.[2]) {
|
|
2617
|
+
const handle = decodeURIComponent(m[1]);
|
|
2618
|
+
const hours = Number(m[2]);
|
|
2619
|
+
const cutoffMs = Date.now() - hours * 3600 * 1e3;
|
|
2620
|
+
const chat = await this.db.findChatByHandle(handle);
|
|
2621
|
+
if (!chat) return { handle, windowHours: hours, messages: [] };
|
|
2622
|
+
const msgs = await this.db.getMessagesForChat(chat.chatIdentifier, 500);
|
|
2623
|
+
return {
|
|
2624
|
+
handle,
|
|
2625
|
+
chatId: chat.chatIdentifier,
|
|
2626
|
+
windowHours: hours,
|
|
2627
|
+
messages: msgs.filter((mm) => mm.date.getTime() >= cutoffMs).map(messageToStructured)
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
if (uri === "contacts://") {
|
|
2631
|
+
const all = this.db.contacts.listContacts(0, 1e4);
|
|
2632
|
+
return { count: all.contacts.length, contacts: all.contacts };
|
|
2633
|
+
}
|
|
2634
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
2635
|
+
}
|
|
2636
|
+
async dispatchTool(name, args, signal) {
|
|
2637
|
+
if (DEV_TOOL_NAMES.has(name) && !isDevMode()) {
|
|
2638
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2639
|
+
}
|
|
2640
|
+
switch (name) {
|
|
2641
|
+
case "get_messages":
|
|
2642
|
+
return await this.handleGetMessages(args);
|
|
2643
|
+
case "get_unread_messages":
|
|
2644
|
+
return await this.handleGetUnreadMessages(args);
|
|
2645
|
+
case "send_message":
|
|
2646
|
+
return await this.handleSendMessage(args);
|
|
2647
|
+
case "wait_for_reply":
|
|
2648
|
+
return await this.handleWaitForReply(args, signal);
|
|
2649
|
+
case "list_conversations":
|
|
2650
|
+
return await this.handleListConversations(args);
|
|
2651
|
+
case "search_messages":
|
|
2652
|
+
return await this.handleSearchMessages(args);
|
|
2653
|
+
case "get_logs":
|
|
2654
|
+
return await this.handleGetLogs(args);
|
|
2655
|
+
case "get_last_send_error":
|
|
2656
|
+
return await this.handleGetLastSendError();
|
|
2657
|
+
case "run_build":
|
|
2658
|
+
return await this.handleRunBuild();
|
|
2659
|
+
case "request_restart":
|
|
2660
|
+
return await this.handleRequestRestart();
|
|
2661
|
+
case "health_check":
|
|
2662
|
+
return await this.handleHealthCheck();
|
|
2663
|
+
case "export_messages":
|
|
2664
|
+
return await this.handleExportMessages(args, signal);
|
|
2665
|
+
case "list_contacts":
|
|
2666
|
+
return await this.handleListContacts(args);
|
|
2667
|
+
case "search_contacts":
|
|
2668
|
+
return await this.handleSearchContacts(args);
|
|
2669
|
+
case "get_contact":
|
|
2670
|
+
return await this.handleGetContact(args);
|
|
2671
|
+
case "resolve_handle":
|
|
2672
|
+
return await this.handleResolveHandle(args);
|
|
2673
|
+
case "check_imessage_availability":
|
|
2674
|
+
return await this.handleCheckImessageAvailability(args);
|
|
2675
|
+
case "search_attachments":
|
|
2676
|
+
return await this.handleSearchAttachments(args);
|
|
2677
|
+
case "get_attachment":
|
|
2678
|
+
return await this.handleGetAttachment(args);
|
|
2679
|
+
case "chat_analytics":
|
|
2680
|
+
return await this.handleChatAnalytics(args);
|
|
2681
|
+
default:
|
|
2682
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
async handleGetLogs(args) {
|
|
2686
|
+
const { tail, source } = GetLogsSchema.parse(args ?? {});
|
|
2687
|
+
const n = tail ?? 50;
|
|
2688
|
+
const sections = [];
|
|
2689
|
+
if (source !== "file") {
|
|
2690
|
+
const memLines = getLogs(n);
|
|
2691
|
+
sections.push(
|
|
2692
|
+
`## In-Memory Logs (${memLines.length} lines)
|
|
2693
|
+
${memLines.length === 0 ? "No log lines yet." : memLines.join("\n")}`
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
if (source === "file" || source === "all") {
|
|
2697
|
+
const fileLines = getFileLogLines(n);
|
|
2698
|
+
const logPath = getLogFilePath() ?? getLogDirectory();
|
|
2699
|
+
sections.push(
|
|
2700
|
+
`## File Logs (${logPath})
|
|
2701
|
+
${fileLines.length === 0 ? "No file log entries." : fileLines.join("\n")}`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
if (source !== "file" && source !== "all") {
|
|
2705
|
+
const logPath = getLogFilePath() ?? getLogDirectory();
|
|
2706
|
+
sections.push(`
|
|
2707
|
+
📁 Full NDJSON logs: ${logPath}`);
|
|
2708
|
+
}
|
|
2709
|
+
return toolText(sections.join("\n\n"), { source: source ?? "memory", tail: n, sections });
|
|
2710
|
+
}
|
|
2711
|
+
async handleGetLastSendError() {
|
|
2712
|
+
const err = getLastSendError();
|
|
2713
|
+
if (!err) {
|
|
2714
|
+
return toolText(
|
|
2715
|
+
"No send failure recorded. Last send either succeeded or occurred before this server run.",
|
|
2716
|
+
{
|
|
2717
|
+
lastSendError: null
|
|
2718
|
+
}
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
const text = [
|
|
2722
|
+
"Last send_message failure:",
|
|
2723
|
+
` message: ${err.message}`,
|
|
2724
|
+
` timestamp: ${err.timestamp}`,
|
|
2725
|
+
err.stderr != null ? ` stderr: ${err.stderr}` : "",
|
|
2726
|
+
err.stdout != null ? ` stdout: ${err.stdout}` : "",
|
|
2727
|
+
err.code != null ? ` code: ${err.code}` : ""
|
|
2728
|
+
].filter(Boolean).join("\n");
|
|
2729
|
+
return toolText(text, { lastSendError: err });
|
|
2730
|
+
}
|
|
2731
|
+
async handleRunBuild() {
|
|
2732
|
+
try {
|
|
2733
|
+
const { stdout, stderr } = await execFileAsync("pnpm", ["build"], {
|
|
2734
|
+
encoding: "utf8",
|
|
2735
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
2736
|
+
cwd: process.cwd()
|
|
2737
|
+
});
|
|
2738
|
+
return toolText(
|
|
2739
|
+
`Build succeeded.
|
|
2740
|
+
|
|
2741
|
+
stdout:
|
|
2742
|
+
${stdout}${stderr ? `
|
|
2743
|
+
|
|
2744
|
+
stderr:
|
|
2745
|
+
${stderr}` : ""}`,
|
|
2746
|
+
{
|
|
2747
|
+
ok: true,
|
|
2748
|
+
stdout,
|
|
2749
|
+
stderr
|
|
2750
|
+
}
|
|
2751
|
+
);
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
const stderr = error.stderr?.toString?.() ?? error.message ?? "";
|
|
2754
|
+
const stdout = error.stdout?.toString?.() ?? "";
|
|
2755
|
+
appendLog("error", "run_build failed", { stderr, stdout });
|
|
2756
|
+
return toolError(`Build failed.
|
|
2757
|
+
|
|
2758
|
+
stdout:
|
|
2759
|
+
${stdout}
|
|
2760
|
+
|
|
2761
|
+
stderr:
|
|
2762
|
+
${stderr}`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
async handleRequestRestart() {
|
|
2766
|
+
const msg = "Restart requested. Please restart the MCP server in your client (e.g. Cursor) to load new code.";
|
|
2767
|
+
setImmediate(() => shutdown(0));
|
|
2768
|
+
return toolText(msg, { restartRequested: true });
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* Returns vital signs in a fixed text format. Designed to never touch the
|
|
2772
|
+
* DB so it returns instantly even when SQL is blocked — that's the whole
|
|
2773
|
+
* point: this tool verifies "the MCP is alive even though queries are slow".
|
|
2774
|
+
*/
|
|
2775
|
+
async handleHealthCheck() {
|
|
2776
|
+
const wd = readWatchdogState();
|
|
2777
|
+
const uptimeMs = Date.now() - wd.startedAt;
|
|
2778
|
+
const idleMs = Date.now() - this.lastActivityTs;
|
|
2779
|
+
let status = "healthy";
|
|
2780
|
+
const issues = [];
|
|
2781
|
+
if (wd.eventLoopP99Ms > 500) {
|
|
2782
|
+
status = "degraded";
|
|
2783
|
+
issues.push(`event-loop lag p99 ${wd.eventLoopP99Ms.toFixed(0)}ms`);
|
|
2784
|
+
}
|
|
2785
|
+
if (wd.eventLoopP99Ms > 5e3) {
|
|
2786
|
+
status = "unhealthy";
|
|
2787
|
+
}
|
|
2788
|
+
if (wd.rssMb > 800) {
|
|
2789
|
+
status = status === "healthy" ? "degraded" : status;
|
|
2790
|
+
issues.push(`RSS ${wd.rssMb}MB`);
|
|
2791
|
+
}
|
|
2792
|
+
if (this.recentErrorCount > 5) {
|
|
2793
|
+
status = status === "healthy" ? "degraded" : status;
|
|
2794
|
+
issues.push(`${this.recentErrorCount} recent errors`);
|
|
2795
|
+
}
|
|
2796
|
+
const lines = [
|
|
2797
|
+
`Status: ${status}`,
|
|
2798
|
+
issues.length ? `Issues: ${issues.join("; ")}` : "",
|
|
2799
|
+
"",
|
|
2800
|
+
`Uptime: ${formatDuration(uptimeMs)}`,
|
|
2801
|
+
`Last activity: ${formatDuration(idleMs)} ago`,
|
|
2802
|
+
`PID: ${process.pid}`,
|
|
2803
|
+
`Node: ${process.version}`,
|
|
2804
|
+
"",
|
|
2805
|
+
`Heap: ${wd.heapMb || round1(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
|
|
2806
|
+
`RSS: ${wd.rssMb || round1(process.memoryUsage().rss / 1024 / 1024)}MB`,
|
|
2807
|
+
`Event-loop p99: ${wd.eventLoopP99Ms.toFixed(1)}ms`,
|
|
2808
|
+
`Event-loop max: ${wd.eventLoopMaxMs.toFixed(1)}ms`,
|
|
2809
|
+
"",
|
|
2810
|
+
`Total tool calls: ${this.toolCallCount}`,
|
|
2811
|
+
`Recent errors: ${this.recentErrorCount}`,
|
|
2812
|
+
`Engine: ${engineLabel()}`
|
|
2813
|
+
].filter((l) => l !== null);
|
|
2814
|
+
return toolText(lines.join("\n"), {
|
|
2815
|
+
status,
|
|
2816
|
+
issues,
|
|
2817
|
+
uptimeMs,
|
|
2818
|
+
idleMs,
|
|
2819
|
+
pid: process.pid,
|
|
2820
|
+
node: process.version,
|
|
2821
|
+
heapMb: wd.heapMb || round1(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
2822
|
+
rssMb: wd.rssMb || round1(process.memoryUsage().rss / 1024 / 1024),
|
|
2823
|
+
eventLoopP99Ms: wd.eventLoopP99Ms,
|
|
2824
|
+
eventLoopMaxMs: wd.eventLoopMaxMs,
|
|
2825
|
+
toolCallCount: this.toolCallCount,
|
|
2826
|
+
recentErrorCount: this.recentErrorCount,
|
|
2827
|
+
engine: engineLabel()
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
async handleGetMessages(args) {
|
|
2831
|
+
const span = perf("tool:get_messages");
|
|
2832
|
+
const parsed = GetMessagesSchema.parse(args);
|
|
2833
|
+
const { chatIdentifier, threadSlug, beforeMessageId } = parsed;
|
|
2834
|
+
const HARD_PAGE_CAP = 5e3;
|
|
2835
|
+
const requested = resolveLimit(parsed.limit);
|
|
2836
|
+
const limit = Math.min(requested, HARD_PAGE_CAP);
|
|
2837
|
+
const wasCapped = requested > HARD_PAGE_CAP;
|
|
2838
|
+
let messages;
|
|
2839
|
+
let threadHeader = "";
|
|
2840
|
+
if (chatIdentifier || threadSlug) {
|
|
2841
|
+
let targetIdentifier = chatIdentifier;
|
|
2842
|
+
if (threadSlug) {
|
|
2843
|
+
const slugRecord = this.db.getSlugRecord(threadSlug);
|
|
2844
|
+
if (!slugRecord) {
|
|
2845
|
+
return toolError(`Unknown thread slug: ${threadSlug}`);
|
|
2846
|
+
}
|
|
2847
|
+
targetIdentifier = slugRecord.chatIdentifier;
|
|
2848
|
+
}
|
|
2849
|
+
messages = await this.db.getMessagesForChat(targetIdentifier, limit, { beforeMessageId });
|
|
2850
|
+
const conv = await this.db.findChatByHandle(targetIdentifier);
|
|
2851
|
+
if (conv) {
|
|
2852
|
+
const name = conv.displayName || conv.rawIdentifier;
|
|
2853
|
+
const ident = conv.displayName ? ` (${conv.rawIdentifier})` : "";
|
|
2854
|
+
const kind = conv.isGroupChat ? "Group" : "1-on-1";
|
|
2855
|
+
threadHeader = `Thread: ${conv.threadSlug} | ${name}${ident} | ${conv.serviceType} | ${kind}
|
|
2856
|
+
|
|
2857
|
+
`;
|
|
2858
|
+
}
|
|
2859
|
+
} else {
|
|
2860
|
+
messages = await this.db.getRecentMessages(limit);
|
|
2861
|
+
}
|
|
2862
|
+
const durMs = span.end({ limit, returned: messages.length });
|
|
2863
|
+
if (messages.length === 0) {
|
|
2864
|
+
return toolText(`${threadHeader}No messages found.`, {
|
|
2865
|
+
messages: [],
|
|
2866
|
+
count: 0,
|
|
2867
|
+
hasMore: false
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
const formatted = messages.map((m) => formatMessage(m)).join("\n");
|
|
2871
|
+
const oldestId = Math.min(...messages.map((m) => m.id));
|
|
2872
|
+
const hasMore = messages.length === limit;
|
|
2873
|
+
const paginationLine = chatIdentifier || threadSlug ? `
|
|
2874
|
+
_Pagination: oldestMessageId=${oldestId}, hasMore=${hasMore}${wasCapped ? ` (capped at ${HARD_PAGE_CAP} per call — use beforeMessageId or export_messages)` : ""}_` : "";
|
|
2875
|
+
const perfLine = `
|
|
2876
|
+
_Engine: TS | Query: ${durMs.toFixed(0)}ms | Messages: ${messages.length}_`;
|
|
2877
|
+
return toolText(
|
|
2878
|
+
`${threadHeader}Found ${messages.length} message(s):
|
|
2879
|
+
|
|
2880
|
+
${formatted}${paginationLine}${perfLine}`,
|
|
2881
|
+
{
|
|
2882
|
+
messages: messages.map(messageToStructured),
|
|
2883
|
+
count: messages.length,
|
|
2884
|
+
hasMore,
|
|
2885
|
+
oldestMessageId: chatIdentifier || threadSlug ? oldestId : void 0
|
|
2886
|
+
}
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
async handleExportMessages(args, signal) {
|
|
2890
|
+
const span = perf("tool:export_messages");
|
|
2891
|
+
const parsed = ExportMessagesSchema.parse(args);
|
|
2892
|
+
const { format, outputPath, since, until, pageSize } = parsed;
|
|
2893
|
+
let chatIdentifier = parsed.chatIdentifier;
|
|
2894
|
+
if (parsed.threadSlug) {
|
|
2895
|
+
const slugRecord = this.db.getSlugRecord(parsed.threadSlug);
|
|
2896
|
+
if (!slugRecord) {
|
|
2897
|
+
return toolError(`Unknown thread slug: ${parsed.threadSlug}`, {
|
|
2898
|
+
threadSlug: parsed.threadSlug
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
chatIdentifier = slugRecord.chatIdentifier;
|
|
2902
|
+
}
|
|
2903
|
+
if (!chatIdentifier) {
|
|
2904
|
+
return toolError("chatIdentifier or threadSlug required");
|
|
2905
|
+
}
|
|
2906
|
+
const pathError = validateExportOutputPath(outputPath);
|
|
2907
|
+
if (pathError) {
|
|
2908
|
+
return toolError(pathError);
|
|
2909
|
+
}
|
|
2910
|
+
const sinceDate = since ? parseUserDate(since) : null;
|
|
2911
|
+
const untilDate = until ? parseUserDate(until) : null;
|
|
2912
|
+
if (since && !sinceDate) {
|
|
2913
|
+
return toolError(`Could not parse 'since': ${since}`);
|
|
2914
|
+
}
|
|
2915
|
+
if (until && !untilDate) {
|
|
2916
|
+
return toolError(`Could not parse 'until': ${until}`);
|
|
2917
|
+
}
|
|
2918
|
+
const result = await streamExport({
|
|
2919
|
+
db: this.db,
|
|
2920
|
+
chatIdentifier,
|
|
2921
|
+
format,
|
|
2922
|
+
outputPath,
|
|
2923
|
+
since: sinceDate,
|
|
2924
|
+
until: untilDate,
|
|
2925
|
+
pageSize,
|
|
2926
|
+
signal
|
|
2927
|
+
});
|
|
2928
|
+
const durMs = span.end({ format, count: result.count, sizeBytes: result.sizeBytes });
|
|
2929
|
+
return toolText(
|
|
2930
|
+
[
|
|
2931
|
+
`Exported ${result.count} message(s) to ${result.savedTo}`,
|
|
2932
|
+
`Format: ${format}`,
|
|
2933
|
+
`Range: ${result.oldest?.toISOString() ?? "(none)"} → ${result.newest?.toISOString() ?? "(none)"}`,
|
|
2934
|
+
`Size: ${(result.sizeBytes / 1024).toFixed(1)} KB`,
|
|
2935
|
+
`_Took ${durMs.toFixed(0)}ms_`
|
|
2936
|
+
].join("\n"),
|
|
2937
|
+
{
|
|
2938
|
+
...result,
|
|
2939
|
+
format,
|
|
2940
|
+
oldest: result.oldest?.toISOString() ?? null,
|
|
2941
|
+
newest: result.newest?.toISOString() ?? null,
|
|
2942
|
+
durationMs: durMs
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
async handleGetUnreadMessages(args) {
|
|
2947
|
+
const { limit } = GetUnreadMessagesSchema.parse(args ?? {});
|
|
2948
|
+
const resolvedLimit = resolveLimit(limit, 100);
|
|
2949
|
+
const messages = await this.db.getUnreadMessages(resolvedLimit + 1);
|
|
2950
|
+
const hasMore = messages.length > resolvedLimit;
|
|
2951
|
+
const results = messages.slice(0, resolvedLimit);
|
|
2952
|
+
if (results.length === 0) {
|
|
2953
|
+
return toolText("No unread messages.", {
|
|
2954
|
+
messages: [],
|
|
2955
|
+
count: 0,
|
|
2956
|
+
hasMore: false,
|
|
2957
|
+
nextOffset: null
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
const formatted = results.map((msg) => {
|
|
2961
|
+
const slug = this.db.getSlugForChatIdentifier(msg.chatId);
|
|
2962
|
+
const label = slug ?? msg.chatId;
|
|
2963
|
+
return formatMessage(msg, label);
|
|
2964
|
+
}).join("\n");
|
|
2965
|
+
return toolText(`Found ${results.length} unread message(s):
|
|
2966
|
+
|
|
2967
|
+
${formatted}`, {
|
|
2968
|
+
messages: results.map(messageToStructured),
|
|
2969
|
+
count: results.length,
|
|
2970
|
+
hasMore,
|
|
2971
|
+
nextOffset: null
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
async handleSendMessage(args) {
|
|
2975
|
+
const { recipient, threadSlug, message, attachments } = SendMessageSchema.parse(args);
|
|
2976
|
+
if (!recipient && !threadSlug) {
|
|
2977
|
+
return toolError("Either recipient or threadSlug is required.");
|
|
2978
|
+
}
|
|
2979
|
+
if (attachments?.length) {
|
|
2980
|
+
for (const p of attachments) {
|
|
2981
|
+
if (!isAbsolute(p)) {
|
|
2982
|
+
return toolError(`Attachment path must be absolute: ${p}`);
|
|
2983
|
+
}
|
|
2984
|
+
if (!existsSync(p)) {
|
|
2985
|
+
return toolError(`Attachment file not found: ${p}`);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
const available = await checkMessagesAvailable();
|
|
2990
|
+
if (!available) {
|
|
2991
|
+
return toolError("Messages.app is not running or accessible.");
|
|
2992
|
+
}
|
|
2993
|
+
let result;
|
|
2994
|
+
let resolvedTarget = recipient ?? threadSlug ?? "";
|
|
2995
|
+
if (threadSlug) {
|
|
2996
|
+
const slugRecord = this.db.getSlugRecord(threadSlug);
|
|
2997
|
+
if (!slugRecord) {
|
|
2998
|
+
return toolError(
|
|
2999
|
+
`Unknown thread slug: ${threadSlug}. Use list_conversations to see available slugs.`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
if (slugRecord.isGroup) {
|
|
3003
|
+
if (slugRecord.displayName && !slugRecord.displayName.startsWith("chat")) {
|
|
3004
|
+
result = await sendToChat(slugRecord.displayName, message);
|
|
3005
|
+
} else {
|
|
3006
|
+
result = await sendToChatId(slugRecord.chatGuid, message);
|
|
3007
|
+
}
|
|
3008
|
+
} else {
|
|
3009
|
+
result = await sendMessageReliable(slugRecord.chatIdentifier, message);
|
|
3010
|
+
if (!result.success) {
|
|
3011
|
+
result = await sendMessageAlt(slugRecord.chatIdentifier, message);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
resolvedTarget = slugRecord.displayName || slugRecord.chatIdentifier;
|
|
3015
|
+
} else {
|
|
3016
|
+
result = await sendMessageReliable(recipient, message);
|
|
3017
|
+
if (!result.success) {
|
|
3018
|
+
result = await sendMessageAlt(recipient, message);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
if (result.success) {
|
|
3022
|
+
const attachmentResults = [];
|
|
3023
|
+
if (attachments?.length) {
|
|
3024
|
+
const targetHandle = threadSlug ? this.db.getSlugRecord(threadSlug)?.chatIdentifier : recipient;
|
|
3025
|
+
const isGroupTarget = threadSlug ? Boolean(this.db.getSlugRecord(threadSlug)?.isGroup) : false;
|
|
3026
|
+
if (isGroupTarget) {
|
|
3027
|
+
for (const p of attachments) {
|
|
3028
|
+
attachmentResults.push({
|
|
3029
|
+
path: p,
|
|
3030
|
+
success: false,
|
|
3031
|
+
error: "Attachment send to group chats not supported (Messages.app limitation)."
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
} else if (targetHandle) {
|
|
3035
|
+
for (const p of attachments) {
|
|
3036
|
+
const r = await sendAttachment(targetHandle, p);
|
|
3037
|
+
attachmentResults.push({
|
|
3038
|
+
path: p,
|
|
3039
|
+
success: r.success,
|
|
3040
|
+
error: r.error
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
const chat = await this.db.findChatByHandle(
|
|
3046
|
+
threadSlug ? this.db.getSlugRecord(threadSlug)?.chatIdentifier ?? "" : recipient
|
|
3047
|
+
);
|
|
3048
|
+
let lastMessageId;
|
|
3049
|
+
if (chat) {
|
|
3050
|
+
const lastMsg = await this.db.getLastMessage(chat.chatIdentifier);
|
|
3051
|
+
lastMessageId = lastMsg?.id;
|
|
3052
|
+
}
|
|
3053
|
+
const attSummary = attachmentResults.length > 0 ? `
|
|
3054
|
+
Attachments: ${attachmentResults.filter((a) => a.success).length}/${attachmentResults.length} delivered` : "";
|
|
3055
|
+
return toolText(
|
|
3056
|
+
`Message sent to ${resolvedTarget} at ${result.timestamp?.toLocaleString()}${chat ? `
|
|
3057
|
+
Thread: ${chat.threadSlug}` : ""}${lastMessageId ? `
|
|
3058
|
+
Last message ID: ${lastMessageId} (use with wait_for_reply)` : ""}${attSummary}`,
|
|
3059
|
+
{
|
|
3060
|
+
success: true,
|
|
3061
|
+
target: resolvedTarget,
|
|
3062
|
+
timestamp: result.timestamp?.toISOString() ?? null,
|
|
3063
|
+
threadSlug: chat?.threadSlug,
|
|
3064
|
+
lastMessageId,
|
|
3065
|
+
attachments: attachmentResults.length > 0 ? attachmentResults : void 0
|
|
3066
|
+
}
|
|
3067
|
+
);
|
|
3068
|
+
} else {
|
|
3069
|
+
appendLog("error", "send_message failed", { recipient: resolvedTarget, error: result.error });
|
|
3070
|
+
return toolError(
|
|
3071
|
+
`Failed to send message: ${result.error}. Use get_last_send_error for details.`,
|
|
3072
|
+
{
|
|
3073
|
+
error: result.error
|
|
3074
|
+
}
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
async handleWaitForReply(args, signal) {
|
|
3079
|
+
const { chatIdentifier, threadSlug, timeoutSeconds, pollIntervalSeconds, afterMessageId } = WaitForReplySchema.parse(args);
|
|
3080
|
+
if (!chatIdentifier && !threadSlug) {
|
|
3081
|
+
return toolError("Either chatIdentifier or threadSlug is required.");
|
|
3082
|
+
}
|
|
3083
|
+
const timeoutMs = timeoutSeconds * 1e3;
|
|
3084
|
+
const pollIntervalMs = pollIntervalSeconds * 1e3;
|
|
3085
|
+
const startTime = Date.now();
|
|
3086
|
+
let chat;
|
|
3087
|
+
if (threadSlug) {
|
|
3088
|
+
const slugRecord = this.db.getSlugRecord(threadSlug);
|
|
3089
|
+
if (!slugRecord) {
|
|
3090
|
+
return toolError(`Unknown thread slug: ${threadSlug}`);
|
|
3091
|
+
}
|
|
3092
|
+
chat = await this.db.findChatByHandle(slugRecord.chatIdentifier);
|
|
3093
|
+
} else {
|
|
3094
|
+
chat = await this.db.findChatByHandle(chatIdentifier);
|
|
3095
|
+
}
|
|
3096
|
+
if (!chat) {
|
|
3097
|
+
return toolError(`Could not find conversation for: ${threadSlug || chatIdentifier}`);
|
|
3098
|
+
}
|
|
3099
|
+
let lastKnownId = afterMessageId;
|
|
3100
|
+
if (!lastKnownId) {
|
|
3101
|
+
const lastMsg = await this.db.getLastMessage(chat.chatIdentifier);
|
|
3102
|
+
lastKnownId = lastMsg?.id || 0;
|
|
3103
|
+
}
|
|
3104
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
3105
|
+
if (signal?.aborted) {
|
|
3106
|
+
return toolError(
|
|
3107
|
+
`Cancelled by client after ${Math.round((Date.now() - startTime) / 1e3)}s`
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
const newMessages = await this.db.getMessagesAfter(chat.chatIdentifier, lastKnownId);
|
|
3111
|
+
if (newMessages.length > 0) {
|
|
3112
|
+
const formatted = newMessages.map((m) => formatMessage(m)).join("\n");
|
|
3113
|
+
return toolText(`Received ${newMessages.length} new message(s):
|
|
3114
|
+
|
|
3115
|
+
${formatted}`, {
|
|
3116
|
+
received: true,
|
|
3117
|
+
messages: newMessages.map(messageToStructured),
|
|
3118
|
+
count: newMessages.length
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
await sleep(pollIntervalMs);
|
|
3122
|
+
}
|
|
3123
|
+
return toolText(
|
|
3124
|
+
`Timeout reached (${timeoutSeconds}s) - no new messages received in conversation with ${threadSlug || chatIdentifier}`,
|
|
3125
|
+
{
|
|
3126
|
+
received: false,
|
|
3127
|
+
timedOut: true,
|
|
3128
|
+
timeoutSeconds,
|
|
3129
|
+
threadSlug,
|
|
3130
|
+
chatIdentifier: chat.chatIdentifier
|
|
3131
|
+
}
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
async handleListConversations(args) {
|
|
3135
|
+
const { limit } = ListConversationsSchema.parse(args);
|
|
3136
|
+
const resolvedLimit = resolveLimit(limit);
|
|
3137
|
+
const limited = await this.db.listConversations(resolvedLimit + 1);
|
|
3138
|
+
const hasMore = limited.length > resolvedLimit;
|
|
3139
|
+
const results = limited.slice(0, resolvedLimit);
|
|
3140
|
+
if (results.length === 0) {
|
|
3141
|
+
return toolText("No conversations found.", {
|
|
3142
|
+
conversations: [],
|
|
3143
|
+
count: 0,
|
|
3144
|
+
hasMore: false,
|
|
3145
|
+
nextOffset: null
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
const formatted = results.map((conv) => {
|
|
3149
|
+
const slug = conv.threadSlug ?? conv.chatIdentifier;
|
|
3150
|
+
let name = conv.displayName || conv.chatIdentifier;
|
|
3151
|
+
if (conv.isGroupChat && !conv.displayName) {
|
|
3152
|
+
const resolved = this.db.resolveParticipantNames(conv.participants);
|
|
3153
|
+
const names = resolved.filter((n, i) => n !== conv.participants[i]);
|
|
3154
|
+
if (names.length > 0) {
|
|
3155
|
+
name = names.length <= 3 ? names.join(", ") : `${names.slice(0, 3).join(", ")} +${names.length - 3}`;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
const ident = conv.displayName && conv.displayName !== conv.rawIdentifier ? ` (${conv.rawIdentifier})` : "";
|
|
3159
|
+
const svc = conv.serviceType === "SMS" ? " [SMS]" : "";
|
|
3160
|
+
const group = conv.isGroupChat ? " [Group]" : "";
|
|
3161
|
+
const lastDate = conv.lastMessageDate ? ` - ${relativeDate(conv.lastMessageDate)}` : "";
|
|
3162
|
+
let snippetText = conv.lastMessageSnippet;
|
|
3163
|
+
if (snippetText) {
|
|
3164
|
+
snippetText = sanitizeUserText(snippetText);
|
|
3165
|
+
}
|
|
3166
|
+
const snippet = snippetText ? ` - "${wrapUntrusted(snippetText.length > 50 ? `${snippetText.slice(0, 47)}...` : snippetText)}"` : "";
|
|
3167
|
+
const unread = conv.unreadCount > 0 ? ` [${conv.unreadCount} unread]` : "";
|
|
3168
|
+
return `• [${slug}] ${name}${ident}${svc}${group}${lastDate}${snippet}${unread}`;
|
|
3169
|
+
}).join("\n");
|
|
3170
|
+
return toolText(`Found ${results.length} conversation(s):
|
|
3171
|
+
|
|
3172
|
+
${formatted}`, {
|
|
3173
|
+
conversations: results.map((conversation) => ({
|
|
3174
|
+
...conversation,
|
|
3175
|
+
lastMessageDate: conversation.lastMessageDate?.toISOString() ?? null,
|
|
3176
|
+
lastMessageSnippet: sanitizeUserText(conversation.lastMessageSnippet)
|
|
3177
|
+
})),
|
|
3178
|
+
count: results.length,
|
|
3179
|
+
hasMore,
|
|
3180
|
+
nextOffset: null
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
async handleSearchMessages(args) {
|
|
3184
|
+
const { query, limit, mode, minScore } = SearchMessagesSchema.parse(args);
|
|
3185
|
+
const resolvedLimit = resolveLimit(limit);
|
|
3186
|
+
const SOFT_CAP = 1e4;
|
|
3187
|
+
let messages;
|
|
3188
|
+
let softCapWarning;
|
|
3189
|
+
if (mode === "fuzzy") {
|
|
3190
|
+
const candidates = await this.db.searchMessages(query, SOFT_CAP);
|
|
3191
|
+
const ranked = rankFuzzy(query, candidates, (m) => m.text ?? "", minScore);
|
|
3192
|
+
messages = ranked.slice(0, resolvedLimit + 1).map((r) => r.item);
|
|
3193
|
+
if (candidates.length >= SOFT_CAP) {
|
|
3194
|
+
softCapWarning = `Candidate pool capped at ${SOFT_CAP} pre-scoring. Tighten the query for more reliable ranking.`;
|
|
3195
|
+
}
|
|
3196
|
+
} else {
|
|
3197
|
+
messages = await this.db.searchMessages(query, resolvedLimit + 1);
|
|
3198
|
+
}
|
|
3199
|
+
const hasMore = messages.length > resolvedLimit;
|
|
3200
|
+
const results = messages.slice(0, resolvedLimit);
|
|
3201
|
+
if (results.length === 0) {
|
|
3202
|
+
return toolText(`No messages found matching "${query}".`, {
|
|
3203
|
+
query,
|
|
3204
|
+
mode,
|
|
3205
|
+
messages: [],
|
|
3206
|
+
count: 0,
|
|
3207
|
+
hasMore: false,
|
|
3208
|
+
nextOffset: null,
|
|
3209
|
+
softCapWarning
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
const formatted = results.map((msg) => {
|
|
3213
|
+
const slug = this.db.getSlugForChatIdentifier(msg.chatId);
|
|
3214
|
+
const label = slug ?? msg.chatId;
|
|
3215
|
+
return formatMessage(msg, label);
|
|
3216
|
+
}).join("\n");
|
|
3217
|
+
return toolText(`Found ${results.length} message(s) matching "${query}":
|
|
3218
|
+
|
|
3219
|
+
${formatted}`, {
|
|
3220
|
+
query,
|
|
3221
|
+
mode,
|
|
3222
|
+
messages: results.map(messageToStructured),
|
|
3223
|
+
count: results.length,
|
|
3224
|
+
hasMore,
|
|
3225
|
+
nextOffset: null,
|
|
3226
|
+
softCapWarning
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
// ── Contact tools ──────────────────────────────────────────────────────
|
|
3230
|
+
async handleListContacts(args) {
|
|
3231
|
+
const { limit, offset } = ListContactsSchema.parse(args);
|
|
3232
|
+
const resolvedLimit = resolveLimit(limit);
|
|
3233
|
+
const SAFETY_CAP = 5e3;
|
|
3234
|
+
const effectiveLimit = Math.min(resolvedLimit, SAFETY_CAP);
|
|
3235
|
+
const { contacts, total } = this.db.contacts.listContacts(offset, effectiveLimit);
|
|
3236
|
+
const hasMore = offset + contacts.length < total;
|
|
3237
|
+
if (contacts.length === 0) {
|
|
3238
|
+
return toolText(`No contacts found${offset > 0 ? ` (offset ${offset})` : ""}.`, {
|
|
3239
|
+
contacts: [],
|
|
3240
|
+
count: 0,
|
|
3241
|
+
hasMore: false,
|
|
3242
|
+
totalCount: total
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
const formatted = contacts.map((c) => {
|
|
3246
|
+
const phones = c.phoneNumbers.length > 0 ? ` 📱 ${c.phoneNumbers.join(", ")}` : "";
|
|
3247
|
+
const emails = c.emails.length > 0 ? ` ✉ ${c.emails.join(", ")}` : "";
|
|
3248
|
+
return `• ${sanitizeUserText(c.displayName)}${phones}${emails}`;
|
|
3249
|
+
}).join("\n");
|
|
3250
|
+
return toolText(`Showing ${contacts.length} of ${total} contact(s):
|
|
3251
|
+
|
|
3252
|
+
${formatted}`, {
|
|
3253
|
+
contacts,
|
|
3254
|
+
count: contacts.length,
|
|
3255
|
+
hasMore,
|
|
3256
|
+
totalCount: total
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
async handleSearchContacts(args) {
|
|
3260
|
+
const { query, limit } = SearchContactsSchema.parse(args);
|
|
3261
|
+
const resolvedLimit = resolveLimit(limit);
|
|
3262
|
+
const all = this.db.contacts.searchContacts(query);
|
|
3263
|
+
const results = resolvedLimit === Number.MAX_SAFE_INTEGER ? all : all.slice(0, resolvedLimit);
|
|
3264
|
+
if (results.length === 0) {
|
|
3265
|
+
return toolText(`No contacts match "${query}".`, {
|
|
3266
|
+
query,
|
|
3267
|
+
contacts: [],
|
|
3268
|
+
count: 0
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
rememberSearch(
|
|
3272
|
+
query,
|
|
3273
|
+
results.map((c) => ({
|
|
3274
|
+
handle: c.phoneNumbers[0] ?? c.emails[0] ?? c.displayName,
|
|
3275
|
+
displayName: c.displayName
|
|
3276
|
+
}))
|
|
3277
|
+
);
|
|
3278
|
+
const formatted = results.map((c, i) => {
|
|
3279
|
+
const phones = c.phoneNumbers.length > 0 ? ` 📱 ${c.phoneNumbers.join(", ")}` : "";
|
|
3280
|
+
const emails = c.emails.length > 0 ? ` ✉ ${c.emails.join(", ")}` : "";
|
|
3281
|
+
return `[contact:${i + 1}] ${sanitizeUserText(c.displayName)}${phones}${emails}`;
|
|
3282
|
+
}).join("\n");
|
|
3283
|
+
const hint = results.length > 1 ? `
|
|
3284
|
+
|
|
3285
|
+
Re-select by index in any contact-accepting tool: handle: "contact:1" … "contact:${results.length}".` : "";
|
|
3286
|
+
return toolText(
|
|
3287
|
+
`Found ${results.length} contact(s) matching "${query}":
|
|
3288
|
+
|
|
3289
|
+
${formatted}${hint}`,
|
|
3290
|
+
{
|
|
3291
|
+
query,
|
|
3292
|
+
contacts: results,
|
|
3293
|
+
count: results.length
|
|
3294
|
+
}
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
async handleGetContact(args) {
|
|
3298
|
+
const { handle, id } = GetContactSchema.parse(args);
|
|
3299
|
+
let contact = null;
|
|
3300
|
+
if (handle !== void 0) {
|
|
3301
|
+
const selectorHit = resolveContactSelector(handle);
|
|
3302
|
+
const effectiveHandle = selectorHit?.handle ?? handle;
|
|
3303
|
+
const lookup = this.db.contacts.lookupContact(effectiveHandle);
|
|
3304
|
+
contact = lookup ? this.db.contacts.getContact(lookup.contactId) : null;
|
|
3305
|
+
} else if (id !== void 0) {
|
|
3306
|
+
contact = this.db.contacts.getContact(id);
|
|
3307
|
+
}
|
|
3308
|
+
if (!contact) {
|
|
3309
|
+
return toolText("Contact not found.", { contact: null });
|
|
3310
|
+
}
|
|
3311
|
+
const phones = contact.phoneNumbers.length > 0 ? `
|
|
3312
|
+
Phones: ${contact.phoneNumbers.join(", ")}` : "";
|
|
3313
|
+
const emails = contact.emails.length > 0 ? `
|
|
3314
|
+
Emails: ${contact.emails.join(", ")}` : "";
|
|
3315
|
+
const org = contact.organization ? `
|
|
3316
|
+
Organization: ${contact.organization}` : "";
|
|
3317
|
+
return toolText(
|
|
3318
|
+
`${sanitizeUserText(contact.displayName)} (id ${contact.id})${phones}${emails}${org}`,
|
|
3319
|
+
{ contact }
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
async handleResolveHandle(args) {
|
|
3323
|
+
const { handle } = ResolveHandleSchema.parse(args);
|
|
3324
|
+
const selectorHit = resolveContactSelector(handle);
|
|
3325
|
+
const effectiveHandle = selectorHit?.handle ?? handle;
|
|
3326
|
+
const lookup = this.db.contacts.lookupContact(effectiveHandle);
|
|
3327
|
+
if (lookup) {
|
|
3328
|
+
return toolText(`${handle} → ${sanitizeUserText(lookup.displayName)}`, {
|
|
3329
|
+
handle,
|
|
3330
|
+
displayName: lookup.displayName,
|
|
3331
|
+
contactId: lookup.contactId,
|
|
3332
|
+
label: lookup.label ?? null,
|
|
3333
|
+
resolved: true
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
return toolText(`No contact for ${handle}.`, {
|
|
3337
|
+
handle,
|
|
3338
|
+
displayName: handle,
|
|
3339
|
+
contactId: null,
|
|
3340
|
+
label: null,
|
|
3341
|
+
resolved: false
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
async handleCheckImessageAvailability(args) {
|
|
3345
|
+
const { handle } = CheckImessageAvailabilitySchema.parse(args);
|
|
3346
|
+
const selectorHit = resolveContactSelector(handle);
|
|
3347
|
+
const effectiveHandle = selectorHit?.handle ?? handle;
|
|
3348
|
+
const result = await checkImessageAvailability(effectiveHandle);
|
|
3349
|
+
const text = result.reachable ? `${handle} reachable via ${result.service}.` : `${handle} not reachable. ${result.hint ?? ""}`.trim();
|
|
3350
|
+
return toolText(text, {
|
|
3351
|
+
handle,
|
|
3352
|
+
service: result.service,
|
|
3353
|
+
reachable: result.reachable,
|
|
3354
|
+
hint: result.hint
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
async handleSearchAttachments(args) {
|
|
3358
|
+
const { mimePrefix, chatIdentifier, since, until, limit } = SearchAttachmentsSchema.parse(args);
|
|
3359
|
+
const sinceMs = since ? parseUserDate(since)?.getTime() : void 0;
|
|
3360
|
+
const untilMs = until ? parseUserDate(until)?.getTime() : void 0;
|
|
3361
|
+
const resolvedLimit = resolveLimit(limit);
|
|
3362
|
+
const opts = {
|
|
3363
|
+
limit: resolvedLimit
|
|
3364
|
+
};
|
|
3365
|
+
if (mimePrefix !== void 0) opts.mimePrefix = mimePrefix;
|
|
3366
|
+
if (chatIdentifier !== void 0) opts.chatIdentifier = chatIdentifier;
|
|
3367
|
+
if (sinceMs !== void 0) opts.sinceMs = sinceMs;
|
|
3368
|
+
if (untilMs !== void 0) opts.untilMs = untilMs;
|
|
3369
|
+
const results = this.db.searchAttachments(opts);
|
|
3370
|
+
const formatted = results.map(
|
|
3371
|
+
(a) => `[${a.rowId}] ${a.mimeType ?? "?"} · ${a.totalBytes}B · ${a.createdDate.toISOString().slice(0, 10)} · ${a.transferName ?? a.filename}`
|
|
3372
|
+
).join("\n");
|
|
3373
|
+
return toolText(`Found ${results.length} attachment(s):
|
|
3374
|
+
|
|
3375
|
+
${formatted}`, {
|
|
3376
|
+
attachments: results.map((a) => ({
|
|
3377
|
+
rowId: a.rowId,
|
|
3378
|
+
filename: a.filename,
|
|
3379
|
+
mimeType: a.mimeType,
|
|
3380
|
+
transferName: a.transferName,
|
|
3381
|
+
totalBytes: a.totalBytes,
|
|
3382
|
+
createdDate: a.createdDate.toISOString(),
|
|
3383
|
+
chatId: a.chatId
|
|
3384
|
+
})),
|
|
3385
|
+
count: results.length
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
async handleGetAttachment(args) {
|
|
3389
|
+
const { readFileSync } = await import("node:fs");
|
|
3390
|
+
const { rowId, inlineMaxBytes } = GetAttachmentSchema.parse(args);
|
|
3391
|
+
const rec = this.db.getAttachmentByRowId(rowId);
|
|
3392
|
+
if (!rec) return toolError(`Attachment ROWID ${rowId} not found.`);
|
|
3393
|
+
const resolvedPath = rec.filename.replace(/^~/, process.env.HOME ?? "~");
|
|
3394
|
+
if (!existsSync(resolvedPath)) {
|
|
3395
|
+
return toolError(`Attachment file does not exist: ${resolvedPath}`);
|
|
3396
|
+
}
|
|
3397
|
+
const stat = statSync(resolvedPath);
|
|
3398
|
+
const sizeBytes = stat.size;
|
|
3399
|
+
const isHeic = (rec.mimeType ?? "").toLowerCase().includes("heic") || resolvedPath.toLowerCase().endsWith(".heic");
|
|
3400
|
+
const inline = sizeBytes <= inlineMaxBytes;
|
|
3401
|
+
let base64;
|
|
3402
|
+
let convertedNote;
|
|
3403
|
+
let finalMime = rec.mimeType;
|
|
3404
|
+
let finalPath = resolvedPath;
|
|
3405
|
+
if (inline) {
|
|
3406
|
+
if (isHeic) {
|
|
3407
|
+
try {
|
|
3408
|
+
const { execFileSync } = await import("node:child_process");
|
|
3409
|
+
const { tmpdir } = await import("node:os");
|
|
3410
|
+
const { join: pjoin } = await import("node:path");
|
|
3411
|
+
const out = pjoin(tmpdir(), `imsg-att-${rowId}.png`);
|
|
3412
|
+
execFileSync("sips", ["-s", "format", "png", resolvedPath, "--out", out], {
|
|
3413
|
+
stdio: "ignore"
|
|
3414
|
+
});
|
|
3415
|
+
finalPath = out;
|
|
3416
|
+
finalMime = "image/png";
|
|
3417
|
+
convertedNote = "HEIC → PNG via sips";
|
|
3418
|
+
base64 = readFileSync(out).toString("base64");
|
|
3419
|
+
} catch (e) {
|
|
3420
|
+
return toolError(`HEIC→PNG conversion failed: ${e.message ?? e}`);
|
|
3421
|
+
}
|
|
3422
|
+
} else {
|
|
3423
|
+
base64 = readFileSync(resolvedPath).toString("base64");
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
return toolText(
|
|
3427
|
+
inline ? `Attachment ${rowId} (${sizeBytes}B, ${finalMime ?? "?"}) returned inline.` : `Attachment ${rowId} too large to inline (${sizeBytes}B > ${inlineMaxBytes}B). Use path: ${resolvedPath}`,
|
|
3428
|
+
{
|
|
3429
|
+
rowId,
|
|
3430
|
+
filename: rec.filename,
|
|
3431
|
+
resolvedPath: finalPath,
|
|
3432
|
+
mimeType: finalMime,
|
|
3433
|
+
totalBytes: sizeBytes,
|
|
3434
|
+
inline,
|
|
3435
|
+
base64,
|
|
3436
|
+
converted: convertedNote
|
|
3437
|
+
}
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
async handleChatAnalytics(args) {
|
|
3441
|
+
const { type, windowDays } = ChatAnalyticsSchema.parse(args);
|
|
3442
|
+
const effectiveDays = type === "year_in_review_wrapped" ? 365 : windowDays;
|
|
3443
|
+
const cutoffMs = Date.now() - effectiveDays * 864e5;
|
|
3444
|
+
const maxRowId = this.db.getMaxMessageRowId();
|
|
3445
|
+
const cacheArgs = { type, windowDays: effectiveDays };
|
|
3446
|
+
const hit = lookupCache(type, cacheArgs, maxRowId);
|
|
3447
|
+
if (hit) {
|
|
3448
|
+
return toolText(`Cached ${type} (computed at ${new Date(hit.computedAt).toISOString()}).`, {
|
|
3449
|
+
type,
|
|
3450
|
+
windowDays: effectiveDays,
|
|
3451
|
+
computedAtIso: new Date(hit.computedAt).toISOString(),
|
|
3452
|
+
fromCache: true,
|
|
3453
|
+
data: hit.data
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
const messages = await this.db.getMessagesInWindow(cutoffMs);
|
|
3457
|
+
const result = dispatchAnalytic(type, messages);
|
|
3458
|
+
const computedAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3459
|
+
storeCache(type, cacheArgs, maxRowId, result.data);
|
|
3460
|
+
return toolText(
|
|
3461
|
+
`Computed ${type} over ${messages.length} messages in the last ${effectiveDays}d.`,
|
|
3462
|
+
{
|
|
3463
|
+
type,
|
|
3464
|
+
windowDays: effectiveDays,
|
|
3465
|
+
computedAtIso,
|
|
3466
|
+
fromCache: false,
|
|
3467
|
+
data: result.data
|
|
3468
|
+
}
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
async run() {
|
|
3472
|
+
installShutdownHandlers();
|
|
3473
|
+
registerCleanup(() => logShutdown("normal"));
|
|
3474
|
+
registerCleanup(() => stopHeapMonitor());
|
|
3475
|
+
registerCleanup(() => this.db.close());
|
|
3476
|
+
enableStdinEofDetection();
|
|
3477
|
+
enableOrphanWatchdog();
|
|
3478
|
+
installWatchdog();
|
|
3479
|
+
logStartup("mcp-server");
|
|
3480
|
+
const transport = new StdioServerTransport();
|
|
3481
|
+
await this.server.connect(transport);
|
|
3482
|
+
info("iMessage MCP Server running on stdio");
|
|
3483
|
+
startHeapMonitor();
|
|
3484
|
+
console.error(`iMessage MCP Server running on stdio (logs: ${getLogDirectory()})`);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
async function runMcpServer() {
|
|
3488
|
+
try {
|
|
3489
|
+
const server = new IMessageMCPServer();
|
|
3490
|
+
await server.run();
|
|
3491
|
+
} catch (error) {
|
|
3492
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3493
|
+
const report = await checkLocalAccess();
|
|
3494
|
+
console.error("");
|
|
3495
|
+
console.error(formatAccessReport(report));
|
|
3496
|
+
await shutdown(1);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
export {
|
|
3500
|
+
IMessageMCPServer,
|
|
3501
|
+
runMcpServer
|
|
3502
|
+
};
|
|
3503
|
+
//# sourceMappingURL=index.js.map
|