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/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, "&lt;/untrusted&gt;").replace(new RegExp(`<\\/instructions uuid="${INSTRUCTIONS_UUID}">`, "gi"), "").replace(/<\/instructions>/gi, "&lt;/instructions&gt;");
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