imsg-mcp 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ # [1.19.0](https://github.com/george43g/imsg-mcp/compare/v1.18.0...v1.19.0) (2026-07-25)
7
+
8
+
9
+ ### Features
10
+
11
+ * **contacts:** attach a normalized identity block to get_contact + resolve_handle ([99dfc72](https://github.com/george43g/imsg-mcp/commit/99dfc723e2a5bf8508f1b3940a63a0aeb684d120))
12
+
13
+ # [1.18.0](https://github.com/george43g/imsg-mcp/compare/v1.17.0...v1.18.0) (2026-07-25)
14
+
15
+
16
+ ### Features
17
+
18
+ * **tools:** uniform completeness metadata across list responses ([d8fd930](https://github.com/george43g/imsg-mcp/commit/d8fd930dfd28f28ac677ec4ffefd1addb39afbf5))
19
+
6
20
  # [1.17.0](https://github.com/george43g/imsg-mcp/compare/v1.16.0...v1.17.0) (2026-07-25)
7
21
 
8
22
 
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { d as checkLocalAccess, f as formatAccessReport, I as IMPLEMENTED_TYPES, A as ANALYTIC_INFO, i as installShutdownHandlers, r as registerCleanup, l as looksLikeThreadSlug } from "./shutdown-0TCv4XJB.js";
7
- import { A as APP_VERSION, t as toYaml } from "./meta-B5cdb7uT.js";
7
+ import { A as APP_VERSION, t as toYaml } from "./meta-CD2uvSHu.js";
8
8
  import { spawn } from "node:child_process";
9
9
  import { join, dirname } from "node:path";
10
10
  function distRoot() {
package/dist/index.js CHANGED
@@ -14,11 +14,11 @@ import { ensureAttachmentDownloaded } from "./attachment-sync-DMvlT8Zk.js";
14
14
  import { n as normalizedPhoneVariants, h as hasNativeModule, I as IMessageDB, r as rankFuzzy } from "./imessage-db-CHbdUXk3.js";
15
15
  import { parseUserDate } from "./date-parse-DJXMfq3a.js";
16
16
  import { streamExport, ExportInterpretGuardError } from "./exportStream-BFxilSlL.js";
17
- import { r as renderAnalyticText, A as APP_VERSION, a as APP_NAME } from "./meta-B5cdb7uT.js";
17
+ import { n as normalizePhoneToE164, m as minMessageId, a as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog, b as noteActivity, r as readWatchdogState } from "./watchdog-D54vuRzh.js";
18
+ import { r as renderAnalyticText, A as APP_VERSION, a as APP_NAME } from "./meta-CD2uvSHu.js";
18
19
  import { randomUUID } from "node:crypto";
19
20
  import { z } from "zod";
20
21
  import { a as applyInlineInterpretations, i as imageBlockFromFile, v as videoPosterFrame, m as mediaMetadata, b as refForAttachment, T as TRANSCRIBE_MAX_BYTES, g as getInterpretRuntime, t as transcriptSourceEnum, d as detectTranscriber } from "./media-intel-runtime-BkxG4bmW.js";
21
- import { m as minMessageId, a as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog, n as noteActivity, r as readWatchdogState } from "./watchdog-CW2SEAmu.js";
22
22
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
23
23
  let dbPath = join(homedir(), ".imsg-mcp", "analytics-cache.db");
24
24
  let db = null;
@@ -323,6 +323,37 @@ function humansHintText(hint) {
323
323
  _Relationship file(s): ${list}_
324
324
  _${hint.guidance}_`;
325
325
  }
326
+ function dedupe(items) {
327
+ const seen = /* @__PURE__ */ new Set();
328
+ const out = [];
329
+ for (const item of items) {
330
+ if (item && !seen.has(item)) {
331
+ seen.add(item);
332
+ out.push(item);
333
+ }
334
+ }
335
+ return out;
336
+ }
337
+ function normalizeEmail(email) {
338
+ return email.trim().toLowerCase();
339
+ }
340
+ function e164OrOriginal(phone, country) {
341
+ return normalizePhoneToE164(phone, country) ?? phone.trim();
342
+ }
343
+ function buildIdentity(canonicalName, phoneNumbers, emails, country) {
344
+ const phones = dedupe(phoneNumbers.map((p) => e164OrOriginal(p, country)));
345
+ const mails = dedupe(emails.map(normalizeEmail));
346
+ return {
347
+ canonicalName,
348
+ phones,
349
+ emails: mails,
350
+ handles: dedupe([...phones, ...mails])
351
+ };
352
+ }
353
+ function buildIdentityFromHandle(handle, name, country) {
354
+ const isEmail = handle.includes("@");
355
+ return buildIdentity(name ?? handle, isEmail ? [] : [handle], isEmail ? [handle] : [], country);
356
+ }
326
357
  const INSTRUCTIONS_UUID = randomUUID();
327
358
  function wrapUntrusted(text) {
328
359
  if (text == null || text === "") return "";
@@ -1826,7 +1857,9 @@ const GetUnreadMessagesOutputSchema = z.object({
1826
1857
  messages: z.array(MessageSchema),
1827
1858
  count: z.number().int(),
1828
1859
  hasMore: z.boolean(),
1829
- nextOffset: z.number().int().nullable()
1860
+ truncated: z.boolean().describe(
1861
+ "True if more unread messages exist than were returned — raise `limit` to get the rest."
1862
+ )
1830
1863
  });
1831
1864
  const SendMessageSchema = z.object({
1832
1865
  recipient: nonEmptyString("Phone number or email address to send to").optional(),
@@ -1886,6 +1919,9 @@ const ListConversationsOutputSchema = z.object({
1886
1919
  conversations: z.array(ConversationSchema),
1887
1920
  count: z.number().int(),
1888
1921
  hasMore: z.boolean(),
1922
+ truncated: z.boolean().describe(
1923
+ "True if more conversations exist beyond this page — page on with `offset` + `nextOffset`."
1924
+ ),
1889
1925
  nextOffset: z.number().int().nullable()
1890
1926
  });
1891
1927
  const SearchMessagesSchema = z.object({
@@ -1904,7 +1940,7 @@ const SearchMessagesOutputSchema = z.object({
1904
1940
  messages: z.array(MessageSchema),
1905
1941
  count: z.number().int(),
1906
1942
  hasMore: z.boolean(),
1907
- nextOffset: z.number().int().nullable(),
1943
+ truncated: z.boolean().describe("True if more matches exist than were returned — raise `limit` or narrow the query."),
1908
1944
  softCapWarning: z.string().optional()
1909
1945
  });
1910
1946
  const GetLogsSchema = z.object({
@@ -1971,7 +2007,9 @@ const ListContactsOutputSchema = z.object({
1971
2007
  contacts: z.array(ContactSchema),
1972
2008
  count: z.number().int(),
1973
2009
  hasMore: z.boolean(),
1974
- totalCount: z.number().int()
2010
+ truncated: z.boolean().describe("True if more contacts exist beyond this page — page on with `offset`."),
2011
+ totalAvailable: z.number().int().describe("Total contacts available across all pages."),
2012
+ totalCount: z.number().int().describe("Alias of `totalAvailable` (kept for back-compat).")
1975
2013
  });
1976
2014
  const RankedContactSchema = ContactSchema.extend({
1977
2015
  score: z.number().describe("Relevance in [0,1]; higher = better. Results are sorted best-first."),
@@ -1984,7 +2022,9 @@ const SearchContactsSchema = z.object({
1984
2022
  const SearchContactsOutputSchema = z.object({
1985
2023
  query: z.string(),
1986
2024
  contacts: z.array(RankedContactSchema),
1987
- count: z.number().int()
2025
+ count: z.number().int(),
2026
+ truncated: z.boolean().describe("True if more contacts matched than were returned — raise `limit`."),
2027
+ totalAvailable: z.number().int().describe("Total contacts that matched the query.")
1988
2028
  });
1989
2029
  const GetContactSchema = z.object({
1990
2030
  handle: z.string().optional().describe("Phone number or email to look up."),
@@ -1992,8 +2032,16 @@ const GetContactSchema = z.object({
1992
2032
  }).refine((v) => v.handle !== void 0 || v.id !== void 0, {
1993
2033
  message: "Provide either `handle` or `id`."
1994
2034
  });
2035
+ const IdentitySchema = z.object({
2036
+ canonicalName: z.string().describe("Best display name for this person (or the handle itself)."),
2037
+ phones: z.array(z.string()).describe("Phone handles, E.164-normalized where derivable."),
2038
+ emails: z.array(z.string()).describe("Email handles, trimmed + lowercased."),
2039
+ handles: z.array(z.string()).describe("Every reachable handle, deduped — phones first, then emails.")
2040
+ });
1995
2041
  const GetContactOutputSchema = z.object({
1996
2042
  contact: ContactSchema.nullable(),
2043
+ /** Normalized cross-tool handle view (E.164 phones, lowercased emails). */
2044
+ identity: IdentitySchema.optional(),
1997
2045
  /** Per-handle conversation mapping: which thread slug each handle chats under. */
1998
2046
  threads: z.array(
1999
2047
  z.object({
@@ -2032,7 +2080,9 @@ const ResolveHandleOutputSchema = z.object({
2032
2080
  displayName: z.string(),
2033
2081
  contactId: z.number().int().nullable(),
2034
2082
  label: z.string().nullable(),
2035
- resolved: z.boolean()
2083
+ resolved: z.boolean(),
2084
+ /** Normalized cross-tool handle view — populated even when unresolved. */
2085
+ identity: IdentitySchema.optional()
2036
2086
  });
2037
2087
  const CheckImessageAvailabilitySchema = z.object({
2038
2088
  handle: nonEmptyString("Phone number or email to preflight-check for reachability.")
@@ -2057,11 +2107,12 @@ const SearchAttachmentsSchema = z.object({
2057
2107
  chatIdentifier: z.string().optional().describe("Restrict to a single chat (use chat_identifier from list_conversations)."),
2058
2108
  since: z.string().optional().describe("ISO date or relative ('1 week ago'). Lower bound on attachment creation."),
2059
2109
  until: z.string().optional().describe("ISO date or relative. Upper bound on creation."),
2060
- limit: z.number().int().min(0).default(20).describe("Max results. 0 = unlimited (capped at 1000).")
2110
+ limit: z.number().int().min(0).default(20).describe("Max results. 0 = unlimited.")
2061
2111
  });
2062
2112
  const SearchAttachmentsOutputSchema = z.object({
2063
2113
  attachments: z.array(AttachmentRecordSchema),
2064
- count: z.number().int()
2114
+ count: z.number().int(),
2115
+ truncated: z.boolean().describe("True if more attachments matched than were returned — raise `limit`.")
2065
2116
  });
2066
2117
  const InitHumanSchema = z.object({
2067
2118
  contact: nonEmptyString(
@@ -2415,7 +2466,7 @@ const TOOLS = [
2415
2466
  },
2416
2467
  {
2417
2468
  name: "search_contacts",
2418
- description: "Substring-match contacts by display name, phone number, or email.",
2469
+ description: "Substring-match contacts by display name, phone number, or email, ranked best-first (score + matchedField). Response reports `truncated` and `totalAvailable` so you know when more matched than were returned — raise `limit` to widen.",
2419
2470
  annotations: annotations.read,
2420
2471
  inputSchema: {
2421
2472
  type: "object",
@@ -2429,7 +2480,7 @@ const TOOLS = [
2429
2480
  },
2430
2481
  {
2431
2482
  name: "get_contact",
2432
- description: "Fetch a single contact by handle (phone/email) or by numeric id, including each handle's thread slug (for send_message/get_messages). Returns null if not found. Includes `humansFile` — the path to this person's humans/v1 relationship file if one exists (read it for relationship context; init_human scaffolds one otherwise).",
2483
+ description: "Fetch a single contact by handle (phone/email) or by numeric id, including each handle's thread slug (for send_message/get_messages) and an `identity` block (canonicalName + E.164-normalized phones + lowercased emails + deduped handles). Returns null if not found. Includes `humansFile` — the path to this person's humans/v1 relationship file if one exists (read it for relationship context; init_human scaffolds one otherwise).",
2433
2484
  annotations: annotations.read,
2434
2485
  inputSchema: {
2435
2486
  type: "object",
@@ -2459,7 +2510,7 @@ const TOOLS = [
2459
2510
  },
2460
2511
  {
2461
2512
  name: "resolve_handle",
2462
- description: "Resolve a phone number or email to its contact display name. Pass-through if unknown.",
2513
+ description: "Resolve a phone number or email to its contact display name. Pass-through if unknown. Always returns an `identity` block (canonicalName + E.164-normalized phones + lowercased emails + deduped handles) — even for an unknown handle, so you get it in canonical form.",
2463
2514
  annotations: annotations.read,
2464
2515
  inputSchema: {
2465
2516
  type: "object",
@@ -2488,7 +2539,7 @@ const TOOLS = [
2488
2539
  },
2489
2540
  {
2490
2541
  name: "search_attachments",
2491
- 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.",
2542
+ 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. Response reports `truncated` (more matched than returned — raise `limit`).",
2492
2543
  annotations: annotations.read,
2493
2544
  inputSchema: {
2494
2545
  type: "object",
@@ -3181,7 +3232,7 @@ ${formatted}${paginationLine}${perfLine}${humansLine}`,
3181
3232
  messages: [],
3182
3233
  count: 0,
3183
3234
  hasMore: false,
3184
- nextOffset: null
3235
+ truncated: false
3185
3236
  });
3186
3237
  }
3187
3238
  const formatted = results.map((msg) => {
@@ -3195,7 +3246,7 @@ ${formatted}`, {
3195
3246
  messages: results.map(messageToStructured),
3196
3247
  count: results.length,
3197
3248
  hasMore,
3198
- nextOffset: null
3249
+ truncated: hasMore
3199
3250
  });
3200
3251
  }
3201
3252
  async handleSendMessage(args) {
@@ -3462,6 +3513,7 @@ ${formatted}${humansLine}`, {
3462
3513
  conversations: [],
3463
3514
  count: 0,
3464
3515
  hasMore: false,
3516
+ truncated: false,
3465
3517
  nextOffset: null
3466
3518
  });
3467
3519
  }
@@ -3501,6 +3553,7 @@ ${formatted}`, {
3501
3553
  }),
3502
3554
  count: results.length,
3503
3555
  hasMore,
3556
+ truncated: hasMore,
3504
3557
  nextOffset: hasMore ? startAfter : null
3505
3558
  });
3506
3559
  }
@@ -3529,7 +3582,7 @@ ${formatted}`, {
3529
3582
  messages: [],
3530
3583
  count: 0,
3531
3584
  hasMore: false,
3532
- nextOffset: null,
3585
+ truncated: false,
3533
3586
  softCapWarning
3534
3587
  });
3535
3588
  }
@@ -3546,7 +3599,7 @@ ${formatted}`, {
3546
3599
  messages: results.map(messageToStructured),
3547
3600
  count: results.length,
3548
3601
  hasMore,
3549
- nextOffset: null,
3602
+ truncated: hasMore,
3550
3603
  softCapWarning
3551
3604
  });
3552
3605
  }
@@ -3563,6 +3616,8 @@ ${formatted}`, {
3563
3616
  contacts: [],
3564
3617
  count: 0,
3565
3618
  hasMore: false,
3619
+ truncated: false,
3620
+ totalAvailable: total,
3566
3621
  totalCount: total
3567
3622
  });
3568
3623
  }
@@ -3577,6 +3632,8 @@ ${formatted}`, {
3577
3632
  contacts,
3578
3633
  count: contacts.length,
3579
3634
  hasMore,
3635
+ truncated: hasMore,
3636
+ totalAvailable: total,
3580
3637
  totalCount: total
3581
3638
  });
3582
3639
  }
@@ -3590,7 +3647,9 @@ ${formatted}`, {
3590
3647
  return toolText(`No contacts match "${query}".`, {
3591
3648
  query,
3592
3649
  contacts: [],
3593
- count: 0
3650
+ count: 0,
3651
+ truncated: false,
3652
+ totalAvailable: 0
3594
3653
  });
3595
3654
  }
3596
3655
  rememberSearch(
@@ -3621,7 +3680,9 @@ ${formatted}${hint}`,
3621
3680
  score: m.score,
3622
3681
  matchedField: m.matchedField
3623
3682
  })),
3624
- count: results.length
3683
+ count: results.length,
3684
+ truncated: ranked.length > results.length,
3685
+ totalAvailable: ranked.length
3625
3686
  }
3626
3687
  );
3627
3688
  }
@@ -3668,10 +3729,17 @@ ${withThreads.map((t) => ` ${t.handle} → ${t.threadSlug}`).join("\n")}` : "";
3668
3729
  const humansLine = humansHint ? humansHintText(humansHint) : `
3669
3730
 
3670
3731
  _${HUMANS_INIT_HINT}_`;
3732
+ const identity = buildIdentity(
3733
+ contact.displayName,
3734
+ contact.phoneNumbers,
3735
+ contact.emails,
3736
+ defaultCountryFromEnv()
3737
+ );
3671
3738
  return toolText(
3672
3739
  `${sanitizeUserText(contact.displayName)} (id ${contact.id})${phones}${emails}${org}${threadLines}${humansLine}`,
3673
3740
  {
3674
3741
  contact,
3742
+ identity,
3675
3743
  threads,
3676
3744
  humansFile,
3677
3745
  humansGuidance: humansHint?.guidance ?? HUMANS_INIT_HINT
@@ -3729,13 +3797,17 @@ ${lines}${hint}`,
3729
3797
  const selectorHit = resolveContactSelector(handle);
3730
3798
  const effectiveHandle = selectorHit?.handle ?? handle;
3731
3799
  const lookup = this.db.contacts.lookupContact(effectiveHandle);
3800
+ const country = defaultCountryFromEnv();
3732
3801
  if (lookup) {
3802
+ const contact = this.db.contacts.getContact(lookup.contactId);
3803
+ const identity = contact ? buildIdentity(contact.displayName, contact.phoneNumbers, contact.emails, country) : buildIdentityFromHandle(effectiveHandle, lookup.displayName, country);
3733
3804
  return toolText(`${handle} → ${sanitizeUserText(lookup.displayName)}`, {
3734
3805
  handle,
3735
3806
  displayName: lookup.displayName,
3736
3807
  contactId: lookup.contactId,
3737
3808
  label: lookup.label ?? null,
3738
- resolved: true
3809
+ resolved: true,
3810
+ identity
3739
3811
  });
3740
3812
  }
3741
3813
  return toolText(`No contact for ${handle}.`, {
@@ -3743,7 +3815,8 @@ ${lines}${hint}`,
3743
3815
  displayName: handle,
3744
3816
  contactId: null,
3745
3817
  label: null,
3746
- resolved: false
3818
+ resolved: false,
3819
+ identity: buildIdentityFromHandle(effectiveHandle, null, country)
3747
3820
  });
3748
3821
  }
3749
3822
  async handleCheckImessageAvailability(args) {
@@ -3774,14 +3847,17 @@ ${lines}${hint}`,
3774
3847
  const sinceMs = since ? parseUserDate(since)?.getTime() : void 0;
3775
3848
  const untilMs = until ? parseUserDate(until)?.getTime() : void 0;
3776
3849
  const resolvedLimit = resolveLimit(limit);
3850
+ const bounded = resolvedLimit < Number.MAX_SAFE_INTEGER;
3777
3851
  const opts = {
3778
- limit: resolvedLimit
3852
+ limit: bounded ? resolvedLimit + 1 : resolvedLimit
3779
3853
  };
3780
3854
  if (mimePrefix !== void 0) opts.mimePrefix = mimePrefix;
3781
3855
  if (chatIdentifier !== void 0) opts.chatIdentifier = chatIdentifier;
3782
3856
  if (sinceMs !== void 0) opts.sinceMs = sinceMs;
3783
3857
  if (untilMs !== void 0) opts.untilMs = untilMs;
3784
- const results = this.db.searchAttachments(opts);
3858
+ const raw = this.db.searchAttachments(opts);
3859
+ const truncated = raw.length > resolvedLimit;
3860
+ const results = raw.slice(0, resolvedLimit);
3785
3861
  const formatted = results.map(
3786
3862
  (a) => `[${a.rowId}] ${a.mimeType ?? "?"} · ${a.totalBytes}B · ${a.createdDate.toISOString().slice(0, 10)} · ${a.transferName ?? a.filename}`
3787
3863
  ).join("\n");
@@ -3797,7 +3873,8 @@ ${formatted}`, {
3797
3873
  createdDate: a.createdDate.toISOString(),
3798
3874
  chatId: a.chatId
3799
3875
  })),
3800
- count: results.length
3876
+ count: results.length,
3877
+ truncated
3801
3878
  });
3802
3879
  }
3803
3880
  async handleGetAttachment(args) {