@mgcrea/mcp-apple-contacts 0.0.0-bootstrap → 1.3.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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { L as BUILD_INFO, M as CONTACTS_SURFACE, a as loadConfig, r as createServer } from "./server-BjWSCaMN.js";
2
+ import { L as BUILD_INFO, M as CONTACTS_SURFACE, a as loadConfig, r as createServer } from "./server-DfeuKOrH.js";
3
3
  import { runStdioServer } from "@mgcrea/mcp-apple-core";
4
4
  //#region src/cli.ts
5
5
  const LOG_PREFIX = "apple-contacts-mcp";
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ declare const BUILD_INFO: BuildInfo;
25
25
  */
26
26
  declare const ConfigSchema: z.ZodObject<{
27
27
  allowWrites: z.ZodDefault<z.ZodBoolean>;
28
+ exposePrompts: z.ZodDefault<z.ZodBoolean>;
28
29
  debug: z.ZodDefault<z.ZodBoolean>;
29
30
  osascriptPath: z.ZodDefault<z.ZodString>;
30
31
  osascriptTimeoutMs: z.ZodDefault<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/build-info.ts","../src/config.ts","../src/client/locate.ts","../src/client/phone.ts","../src/client/store.ts","../src/client/resolve.ts","../src/client/contacts.ts","../src/client/errors.ts","../src/client/ref.ts","../src/server.ts","../src/tools/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/build-info.ts","../src/config.ts","../src/client/locate.ts","../src/client/phone.ts","../src/client/store.ts","../src/client/resolve.ts","../src/client/contacts.ts","../src/client/errors.ts","../src/client/ref.ts","../src/server.ts","../src/tools/index.ts"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as AppleContactsError, C as isShortcode, D as STORE_FILENAME, E as SOURCES_DIRNAME, F as IndexUnavailableError, I as SchemaDriftError, L as BUILD_INFO, M as CONTACTS_SURFACE, N as ContactNotFoundError, O as defaultDirPath, P as ContactsUnavailableError, S as handleKind, T as ADDRESSBOOK_DIR, _ as resolveHandles, a as loadConfig, b as digitsOf, c as decodeRef, d as ContactsIndex, f as countContacts, g as resolveHandle, h as openShard, i as registerTools, j as CONTACTS_BUNDLE_ID, k as locateStores, l as encodeRef, m as introspect, n as SERVER_VERSION, o as InvalidContactRefError, p as displayNameOf, r as createServer, s as REF_VERSION, t as SERVER_NAME, u as AppleContactsClient, v as summarise, w as suffixKey, x as emailKey, y as SUFFIX_DIGITS } from "./server-BjWSCaMN.js";
1
+ import { A as AppleContactsError, C as isShortcode, D as STORE_FILENAME, E as SOURCES_DIRNAME, F as IndexUnavailableError, I as SchemaDriftError, L as BUILD_INFO, M as CONTACTS_SURFACE, N as ContactNotFoundError, O as defaultDirPath, P as ContactsUnavailableError, S as handleKind, T as ADDRESSBOOK_DIR, _ as resolveHandles, a as loadConfig, b as digitsOf, c as decodeRef, d as ContactsIndex, f as countContacts, g as resolveHandle, h as openShard, i as registerTools, j as CONTACTS_BUNDLE_ID, k as locateStores, l as encodeRef, m as introspect, n as SERVER_VERSION, o as InvalidContactRefError, p as displayNameOf, r as createServer, s as REF_VERSION, t as SERVER_NAME, u as AppleContactsClient, v as summarise, w as suffixKey, x as emailKey, y as SUFFIX_DIGITS } from "./server-DfeuKOrH.js";
2
2
  export { ADDRESSBOOK_DIR, AppleContactsClient, AppleContactsError, BUILD_INFO, CONTACTS_BUNDLE_ID, CONTACTS_SURFACE, ContactNotFoundError, ContactsIndex, ContactsUnavailableError, IndexUnavailableError, InvalidContactRefError, REF_VERSION, SERVER_NAME, SERVER_VERSION, SOURCES_DIRNAME, STORE_FILENAME, SUFFIX_DIGITS, SchemaDriftError, countContacts, createServer, decodeRef, defaultDirPath, digitsOf, displayNameOf, emailKey, encodeRef, handleKind, introspect, isShortcode, loadConfig, locateStores, openShard, registerTools, resolveHandle, resolveHandles, suffixKey, summarise };
@@ -1,4 +1,4 @@
1
- import { AppleAutomationError, AppleAutomationError as AppleContactsError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, IndexUnavailableError as IndexUnavailableError$1, SchemaDriftError, SchemaDriftError as SchemaDriftError$1, columnsOf, createOsascriptRunner, describeStore, escapeLike, fail, fingerprintSchema, limitArg, ok, openReadOnly, parseBool, parseConfig, parseIntOpt, readPackageIdentity, trimmed, withBusyRetry, wrap } from "@mgcrea/mcp-apple-core";
1
+ import { AppleAutomationError, AppleAutomationError as AppleContactsError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, IndexUnavailableError as IndexUnavailableError$1, SchemaDriftError, SchemaDriftError as SchemaDriftError$1, columnsOf, createOsascriptRunner, describeStore, escapeLike, fail, fingerprintSchema, limitArg, ok, openReadOnly, parseBool, parseConfig, parseIntOpt, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, requiredPromptArg, trimmed, withBusyRetry, wrap, wrapResult } from "@mgcrea/mcp-apple-core";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
@@ -12,8 +12,8 @@ const pkg = readPackageIdentity(new URL("../package.json", import.meta.url), {
12
12
  const BUILD_INFO = {
13
13
  name: pkg.name,
14
14
  version: pkg.version,
15
- gitCommit: "df06e7f",
16
- gitCommitDate: "2026-08-22T16:20:23+02:00"
15
+ gitCommit: "8248fc3",
16
+ gitCommitDate: "2026-08-26T11:35:39+02:00"
17
17
  };
18
18
  //#endregion
19
19
  //#region src/client/errors.ts
@@ -1193,6 +1193,7 @@ const ConfigSchema = BaseConfigSchema.extend({
1193
1193
  }).strict();
1194
1194
  const loadConfig = (env = process.env) => parseConfig(ConfigSchema, {
1195
1195
  allowWrites: parseBool(env.APPLE_CONTACTS_ALLOW_WRITES),
1196
+ exposePrompts: parseBool(env.APPLE_CONTACTS_EXPOSE_PROMPTS),
1196
1197
  debug: parseBool(env.APPLE_CONTACTS_DEBUG),
1197
1198
  storePath: trimmed(env.APPLE_CONTACTS_STORE),
1198
1199
  indexMode: trimmed(env.APPLE_CONTACTS_INDEX_MODE),
@@ -1202,6 +1203,163 @@ const loadConfig = (env = process.env) => parseConfig(ConfigSchema, {
1202
1203
  maxResults: parseIntOpt(env.APPLE_CONTACTS_MAX_RESULTS)
1203
1204
  });
1204
1205
  //#endregion
1206
+ //#region src/guide.ts
1207
+ /**
1208
+ * The Contacts operating manual, served as `cupertino://contacts/guide` and
1209
+ * embedded ahead of every Contacts prompt. Static by design — see the note in
1210
+ * the Mail guide.
1211
+ */
1212
+ const CONTACTS_GUIDE = `# Apple Contacts — how to drive this server
1213
+
1214
+ ## This server is read-only by construction
1215
+
1216
+ It cannot create, edit or delete a contact, and enabling writes does not add a
1217
+ tool. If someone asks you to update a contact, say that plainly rather than
1218
+ looking for a tool that is not there.
1219
+
1220
+ ## Which tool, under which constraint
1221
+
1222
+ - **A raw phone number or email address** — from a Messages handle, a caller ID,
1223
+ a mail header — is \`apple_contacts_resolve_handles\`. Give it the identifiers
1224
+ as they came; it batches and returns one result per handle.
1225
+ - **A name, or part of one** is \`apple_contacts_search_contacts\`.
1226
+ - **\`apple_contacts_get_contact\`** returns one card in full.
1227
+
1228
+ Phone matching is by trailing digits, because Contacts stores numbers as typed
1229
+ ("06 12 34 56 78") while most systems hand you E.164 ("+33612345678"). Exact
1230
+ string matching resolves almost nothing and is not used.
1231
+
1232
+ ## Read \`status\`, never just \`name\`
1233
+
1234
+ Every resolve result carries a status, and three of the four are not a name:
1235
+
1236
+ - **\`resolved\`** — exactly one contact. \`name\` is set.
1237
+ - **\`unknown\`** — nobody in the address book has this handle. **Common and not
1238
+ an error**: measured on a real store, about one in six of even the busiest
1239
+ correspondents does not resolve. Show the raw handle.
1240
+ - **\`ambiguous\`** — more than one contact has it, so **no name is returned**.
1241
+ Do not guess, and do not pick the first; \`matches\` says how many. Putting the
1242
+ wrong name on a message is worse than putting none, because it does not look
1243
+ wrong.
1244
+ - **\`shortcode\`** — a bank, a courier, a 2FA sender. Can never be a contact.
1245
+
1246
+ ## Permissions and shape
1247
+
1248
+ Contacts has its **own** privacy permission and is not covered by Full Disk
1249
+ Access — and unlike Full Disk Access, macOS prompts for it. A store that cannot
1250
+ be opened usually means that prompt was dismissed: System Settings > Privacy &
1251
+ Security > Contacts.
1252
+
1253
+ The address book is several databases, one per account. All readable ones are
1254
+ unioned; if diagnostics reports fewer opened than found, some accounts are
1255
+ missing from every answer. A contact held in two accounts is folded onto its
1256
+ link id, matching the single card Contacts.app shows.
1257
+ `;
1258
+ //#endregion
1259
+ //#region src/prompts.ts
1260
+ const CTX = {
1261
+ surface: "contacts",
1262
+ guide: CONTACTS_GUIDE
1263
+ };
1264
+ /**
1265
+ * Contacts' one workflow prompt.
1266
+ *
1267
+ * There is one because there is one hard thing on this surface, and it is not
1268
+ * looking a name up — it is refusing to. An ambiguous handle returns no name on
1269
+ * purpose, and the failure this prompt exists to prevent is a model helpfully
1270
+ * choosing the first of three matches and presenting it as fact.
1271
+ */
1272
+ const registerPrompts = (server) => {
1273
+ registerWorkflowPrompt(server, CTX, {
1274
+ name: "apple_contacts_who_is",
1275
+ title: "Who is this",
1276
+ description: "Identify a person from a name, a phone number or an email address, reporting honestly when the address book cannot say. Read-only.",
1277
+ argsSchema: { who: requiredPromptArg("A name, phone number or email address — however it arrived, e.g. \"+33612345678\".") },
1278
+ build: ({ who }) => `Who is: ${who}
1279
+
1280
+ 1. If that looks like a phone number or an email address, use
1281
+ \`apple_contacts_resolve_handles\` — pass it exactly as given, without
1282
+ reformatting it. If it looks like a name, use
1283
+ \`apple_contacts_search_contacts\`.
1284
+ 2. **Read \`status\` before you read \`name\`.** Report each case as what it is:
1285
+ - \`resolved\` — give the name.
1286
+ - \`unknown\` — say the address book does not have this handle and show it
1287
+ raw. This is normal, not a failure, and not worth apologising for.
1288
+ - \`ambiguous\` — say how many people share it and that you will not guess.
1289
+ Offer to show the candidates. **Never pick one.** A confidently wrong name
1290
+ is the one outcome here that nobody catches.
1291
+ - \`shortcode\` — say it is an automated sender, not a person.
1292
+ 3. On a resolved contact, \`apple_contacts_get_contact\` for the full card if the
1293
+ user wants more than the name.
1294
+
1295
+ If nothing resolves at all, check \`cupertino://contacts/diagnostics\` before
1296
+ concluding the address book is empty — Contacts has its own permission, separate
1297
+ from Full Disk Access, and a dismissed prompt looks exactly like no contacts.`
1298
+ });
1299
+ };
1300
+ //#endregion
1301
+ //#region src/tools/diagnostics.ts
1302
+ /**
1303
+ * Build the report.
1304
+ *
1305
+ * Split out of the tool registration so the `cupertino://contacts/diagnostics`
1306
+ * resource can serve the same bytes. Two renderings of one probe: duplicated,
1307
+ * the resource and the tool would drift, and the disagreement would surface as
1308
+ * "the diagnostics lied" — the one thing this file must never do.
1309
+ */
1310
+ const buildDiagnostics = async (client) => {
1311
+ const status = client.status();
1312
+ const located = status.located;
1313
+ return {
1314
+ server: {
1315
+ name: BUILD_INFO.name,
1316
+ version: BUILD_INFO.version
1317
+ },
1318
+ settings: { exposePrompts: client.config.exposePrompts },
1319
+ lane: {
1320
+ reads: "file lane (read-only SQLite)",
1321
+ writes: "none — this server registers no mutating tool",
1322
+ appleEvents: "not used at all, so no Automation grant is needed or requested"
1323
+ },
1324
+ stores: {
1325
+ directory: located.dirPath,
1326
+ directoryListable: located.dirListable,
1327
+ found: located.candidates.length,
1328
+ opened: status.shards.length,
1329
+ sourcesSeen: located.sourceCount,
1330
+ totalContacts: status.totalContacts,
1331
+ shards: status.shards,
1332
+ indexMode: status.indexMode,
1333
+ reason: located.reason
1334
+ },
1335
+ resolution: {
1336
+ phoneSuffixDigits: client.config.phoneSuffixDigits,
1337
+ note: "Phone matching uses the last N digits because Contacts stores numbers as typed. Fewer digits resolves more handles and collides more; the ambiguous count in a resolve result is how to tell whether a change helped."
1338
+ },
1339
+ caveats: [
1340
+ "Contacts is protected by its own privacy permission, NOT by Full Disk Access, and unlike Full Disk Access macOS prompts for it. A store that cannot be opened usually means that prompt was dismissed — re-enable this app under System Settings > Privacy & Security > Contacts.",
1341
+ "The address book is spread across several databases: one per account, plus a root store that is normally almost empty. All readable ones are unioned. If `opened` is lower than `found`, some accounts are missing from every answer here.",
1342
+ "A handle that resolves to more than one contact is reported as ambiguous with no name, never as a guess. Putting the wrong name on a message is worse than putting none, because it does not look wrong.",
1343
+ "Contacts held in two accounts are folded together on their link id, matching what Contacts.app shows as one unified card. A contact with no link id is not folded.",
1344
+ "This server is read-only by construction. It cannot create, edit or delete a contact, and enabling writes does not add a tool."
1345
+ ]
1346
+ };
1347
+ };
1348
+ /**
1349
+ * What this server can currently do, and why not more.
1350
+ *
1351
+ * The caveats are the point. Two of them are specific to this surface and both
1352
+ * produce a plausible wrong answer rather than an error, which is exactly the
1353
+ * kind of thing a caller cannot discover for itself.
1354
+ */
1355
+ const registerDiagnosticsTools = (server, client) => {
1356
+ server.registerTool("apple_contacts_diagnostics", {
1357
+ description: "Report which Contacts stores were opened, how many contacts each holds, and what this server cannot do. Start here when a lookup returns nothing.",
1358
+ inputSchema: {},
1359
+ annotations: { readOnlyHint: true }
1360
+ }, async () => wrap(() => buildDiagnostics(client)));
1361
+ };
1362
+ //#endregion
1205
1363
  //#region src/tools/actions.ts
1206
1364
  /**
1207
1365
  * The mutating tools. Registered only when `allowWrites` is on, and never
@@ -1240,7 +1398,7 @@ const registerActionTools = (server, client) => {
1240
1398
  destructiveHint: false,
1241
1399
  idempotentHint: false
1242
1400
  }
1243
- }, async ({ phones, emails, ...rest }) => wrap(async () => {
1401
+ }, async ({ phones, emails, ...rest }) => wrapResult(async () => {
1244
1402
  if (!rest.firstName && !rest.lastName && !rest.organization) return fail("A contact needs at least one of firstName, lastName or organization — Contacts will otherwise create a nameless card that is hard to find again.");
1245
1403
  return ok(await client.createContact({
1246
1404
  fields: rest,
@@ -1261,7 +1419,7 @@ const registerActionTools = (server, client) => {
1261
1419
  destructiveHint: false,
1262
1420
  idempotentHint: false
1263
1421
  }
1264
- }, async ({ ref, phones, emails, ...rest }) => wrap(async () => {
1422
+ }, async ({ ref, phones, emails, ...rest }) => wrapResult(async () => {
1265
1423
  const decoded = decodeRef(ref);
1266
1424
  const contact = client.get(decoded.source, decoded.recordPk);
1267
1425
  if (!contact) return fail(`No contact for ref "${ref}". It was probably deleted, or the account holding it was removed, since the search ran. Re-run the search to get a current ref.`);
@@ -1311,7 +1469,7 @@ const registerContactTools = (server, client) => {
1311
1469
  description: "One contact in full: name, organisation, job title, every phone number and every email address, each with its label.",
1312
1470
  inputSchema: { ref: z.string().min(1).describe("An opaque contact ref from a list or search result (looks like \"k1:<account>/<id>\"). Do not construct one by hand.") },
1313
1471
  annotations: { readOnlyHint: true }
1314
- }, async ({ ref }) => wrap(async () => {
1472
+ }, async ({ ref }) => wrapResult(async () => {
1315
1473
  const decoded = decodeRef(ref);
1316
1474
  const contact = client.get(decoded.source, decoded.recordPk);
1317
1475
  if (!contact) return fail(`No contact for ref "${ref}". It was probably deleted, or the account holding it was removed, since the search ran. Re-run the search to get a current ref.`);
@@ -1331,58 +1489,6 @@ const registerContactTools = (server, client) => {
1331
1489
  }));
1332
1490
  };
1333
1491
  //#endregion
1334
- //#region src/tools/diagnostics.ts
1335
- /**
1336
- * What this server can currently do, and why not more.
1337
- *
1338
- * The caveats are the point. Two of them are specific to this surface and both
1339
- * produce a plausible wrong answer rather than an error, which is exactly the
1340
- * kind of thing a caller cannot discover for itself.
1341
- */
1342
- const registerDiagnosticsTools = (server, client) => {
1343
- server.registerTool("apple_contacts_diagnostics", {
1344
- description: "Report which Contacts stores were opened, how many contacts each holds, and what this server cannot do. Start here when a lookup returns nothing.",
1345
- inputSchema: {},
1346
- annotations: { readOnlyHint: true }
1347
- }, async () => wrap(async () => {
1348
- const status = client.status();
1349
- const located = status.located;
1350
- return {
1351
- server: {
1352
- name: BUILD_INFO.name,
1353
- version: BUILD_INFO.version
1354
- },
1355
- lane: {
1356
- reads: "file lane (read-only SQLite)",
1357
- writes: "none — this server registers no mutating tool",
1358
- appleEvents: "not used at all, so no Automation grant is needed or requested"
1359
- },
1360
- stores: {
1361
- directory: located.dirPath,
1362
- directoryListable: located.dirListable,
1363
- found: located.candidates.length,
1364
- opened: status.shards.length,
1365
- sourcesSeen: located.sourceCount,
1366
- totalContacts: status.totalContacts,
1367
- shards: status.shards,
1368
- indexMode: status.indexMode,
1369
- reason: located.reason
1370
- },
1371
- resolution: {
1372
- phoneSuffixDigits: client.config.phoneSuffixDigits,
1373
- note: "Phone matching uses the last N digits because Contacts stores numbers as typed. Fewer digits resolves more handles and collides more; the ambiguous count in a resolve result is how to tell whether a change helped."
1374
- },
1375
- caveats: [
1376
- "Contacts is protected by its own privacy permission, NOT by Full Disk Access, and unlike Full Disk Access macOS prompts for it. A store that cannot be opened usually means that prompt was dismissed — re-enable this app under System Settings > Privacy & Security > Contacts.",
1377
- "The address book is spread across several databases: one per account, plus a root store that is normally almost empty. All readable ones are unioned. If `opened` is lower than `found`, some accounts are missing from every answer here.",
1378
- "A handle that resolves to more than one contact is reported as ambiguous with no name, never as a guess. Putting the wrong name on a message is worse than putting none, because it does not look wrong.",
1379
- "Contacts held in two accounts are folded together on their link id, matching what Contacts.app shows as one unified card. A contact with no link id is not folded.",
1380
- "This server is read-only by construction. It cannot create, edit or delete a contact, and enabling writes does not add a tool."
1381
- ]
1382
- };
1383
- }));
1384
- };
1385
- //#endregion
1386
1492
  //#region src/tools/resolve.ts
1387
1493
  /**
1388
1494
  * The tool this surface was built for.
@@ -1469,6 +1575,15 @@ const createServer = (opts) => {
1469
1575
  ...opts.home ? { home: opts.home } : {}
1470
1576
  });
1471
1577
  registerTools(server, client, { allowWrites: config.allowWrites });
1578
+ if (config.exposePrompts) {
1579
+ registerPrompts(server);
1580
+ registerSurfaceResources(server, {
1581
+ surface: "contacts",
1582
+ displayName: "Contacts",
1583
+ guide: CONTACTS_GUIDE,
1584
+ diagnostics: () => buildDiagnostics(client)
1585
+ });
1586
+ }
1472
1587
  return {
1473
1588
  server,
1474
1589
  client
@@ -1477,4 +1592,4 @@ const createServer = (opts) => {
1477
1592
  //#endregion
1478
1593
  export { AppleContactsError as A, isShortcode as C, STORE_FILENAME as D, SOURCES_DIRNAME as E, IndexUnavailableError$1 as F, SchemaDriftError$1 as I, BUILD_INFO as L, CONTACTS_SURFACE as M, ContactNotFoundError as N, defaultDirPath as O, ContactsUnavailableError as P, handleKind as S, ADDRESSBOOK_DIR as T, resolveHandles as _, loadConfig as a, digitsOf as b, decodeRef as c, ContactsIndex as d, countContacts as f, resolveHandle as g, openShard as h, registerTools as i, CONTACTS_BUNDLE_ID as j, locateStores as k, encodeRef as l, introspect as m, SERVER_VERSION as n, InvalidContactRefError as o, displayNameOf as p, createServer as r, REF_VERSION as s, SERVER_NAME as t, AppleContactsClient as u, summarise as v, suffixKey as w, emailKey as x, SUFFIX_DIGITS as y };
1479
1594
 
1480
- //# sourceMappingURL=server-BjWSCaMN.js.map
1595
+ //# sourceMappingURL=server-DfeuKOrH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DfeuKOrH.js","names":["#col","#entFilter","#contactsFrom","#childRows","#config","#logger","#home","#runner","#located","#indexTried","#index","#require","#lookup","#run","#invalidate","#shapeWrite"],"sources":["../src/build-info.ts","../src/client/errors.ts","../src/client/jxa/core.ts","../src/client/jxa/write.ts","../src/client/locate.ts","../src/client/phone.ts","../src/client/resolve.ts","../src/client/store.ts","../src/client/contacts.ts","../src/client/ref.ts","../src/config.ts","../src/guide.ts","../src/prompts.ts","../src/tools/diagnostics.ts","../src/tools/actions.ts","../src/tools/contacts.ts","../src/tools/resolve.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readPackageIdentity, type BuildInfo } from \"@mgcrea/mcp-apple-core\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\nconst pkg = readPackageIdentity(new URL(\"../package.json\", import.meta.url), {\n name: \"@mgcrea/mcp-apple-contacts\",\n version: \"0.0.0\",\n});\n\nexport type { BuildInfo };\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","/**\n * Contacts' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\nexport const CONTACTS_SURFACE: SurfaceContext = {\n appName: \"Contacts\",\n envPrefix: \"APPLE_CONTACTS\",\n};\n\n/**\n * Contacts' Apple Events target — and, like Calendar's `com.apple.iCal`, not the\n * display name. Contacts.app kept the id it shipped with as Address Book;\n * `com.apple.Contacts` does not exist.\n *\n * Used by the write lane only. Reads never send an Apple Event.\n */\nexport const CONTACTS_BUNDLE_ID = \"com.apple.AddressBook\";\n\nexport {\n AppleAutomationError as AppleContactsError,\n AppBusyError as ContactsBusyError,\n AppNotRunningError as ContactsNotRunningError,\n IndexUnavailableError,\n OsascriptTimeoutError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/** A contact ref no longer resolves — deleted, or its account was removed. */\nexport class ContactNotFoundError extends AppleAutomationError {\n override readonly name = \"ContactNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it was ` +\n `removed, since the search ran. Re-run the search to get a current ref.`,\n { ref },\n );\n }\n}\n\n/**\n * The store could not be read, with the reason spelled out.\n *\n * Its own error because Contacts fails differently from every other surface in\n * this repo: it sits behind its own TCC service rather than behind Full Disk\n * Access, and unlike Full Disk Access that permission PROMPTS. So the fix is\n * usually \"answer the dialog\", not \"go to System Settings\" — and telling\n * somebody to grant whole-disk access for an address book would be asking for\n * far more than this server needs.\n */\nexport class ContactsUnavailableError extends AppleAutomationError {\n override readonly name = \"ContactsUnavailableError\";\n\n constructor(reason: string) {\n super(reason, {});\n }\n}\n\n/**\n * A write did not survive the save.\n *\n * Its own error because Contacts fails this way and the other surfaces do not:\n * changes sit in an unsaved buffer until `save()` runs, so a mutation can\n * succeed, read back correctly inside the same script, and still never reach the\n * store. Every write script saves and then re-reads; this is what it raises when\n * the re-read comes back empty.\n */\nexport class ContactWriteNotPersistedError extends AppleAutomationError {\n override readonly name = \"ContactWriteNotPersistedError\";\n\n constructor(message: string) {\n super(\n `${message} Contacts keeps edits in an unsaved buffer, so this usually means the save was ` +\n `refused — check whether Contacts has a modal sheet open, or an account that is read-only.`,\n {},\n );\n }\n}\n\n/**\n * Deleting a contact is not offered, and this says why rather than 404ing.\n *\n * MEASURED, macOS 26.6: the string \"delete\" does not appear anywhere in\n * `sdef /System/Applications/Contacts.app`. The whole command list is make, add,\n * remove and save — `remove` takes a person out of a GROUP, it does not delete\n * them. Writes go through Apple Events on every surface in this repo because the\n * store is `PRAGMA query_only`, so no dictionary verb means no capability.\n */\nexport class ContactDeleteUnsupportedError extends AppleAutomationError {\n override readonly name = \"ContactDeleteUnsupportedError\";\n\n constructor() {\n super(\n `Contacts cannot delete a contact through Apple Events — its scripting dictionary has no ` +\n `delete command at all (make, add, remove and save are the whole list, and \"remove\" only ` +\n `takes someone out of a group). Deleting has to be done in Contacts.app. This server will ` +\n `not write to the address book database directly: it is owned by Contacts and reconciled ` +\n `against iCloud, so writing to it corrupts sync state.`,\n {},\n );\n }\n}\n","/**\n * JXA script fragments for Contacts.\n *\n * Every script here is a static constant. None may contain a template\n * interpolation — `assertStaticScript` rejects any script containing a dollar\n * sign followed by a brace, including template literals written INSIDE the JXA\n * source. Use string concatenation in JXA code.\n *\n * Every script follows the same contract:\n * - it reads its parameters from `JSON.parse(argv[0])`\n * - it returns `JSON.stringify({ok: true, data})` on success\n * - it returns `JSON.stringify({ok: false, error: {code, message}})` on an\n * application-level failure, still exiting 0\n * so a non-zero exit always means infrastructure rather than \"no such contact\".\n *\n * ## There is no read.ts here, and that is still the design\n *\n * Adding writes did not add a read lane. Reads come off the file lane, which is\n * the only place a suffix-keyed index over 970 phone numbers can be built at\n * all — see `../store.ts`. These scripts exist only to make Contacts CHANGE\n * something. `test/jxa.test.ts` asserts `read.ts` does not exist.\n *\n * ## What the dictionary actually offers\n *\n * MEASURED from `sdef /System/Applications/Contacts.app` on macOS 26.6. The\n * whole command list is four verbs:\n *\n * make create a person, or a phone/email element under one\n * add put a person in a group\n * remove take a person out of a group\n * save commit everything\n *\n * That is all of it. The Standard Suite here contains **only `make`** — the\n * string \"delete\" does not appear anywhere in the dictionary. So there is no\n * supported way to delete a contact over Apple Events, which is why this file\n * has no DELETE script and why `delete_contacts` is not a tool. Whether Cocoa\n * Scripting answers an undeclared `delete` event anyway is a separate question\n * and an unmeasured one; guessing at it would risk destroying a real person's\n * card on the strength of an assumption.\n *\n * ## `save` is explicit, global, and the whole reason this is not like Calendar\n *\n * Calendar and Reminders persist a property assignment immediately. Contacts\n * does not: changes sit in an unsaved buffer until `Application(\"Contacts\").save()`\n * runs, and the dictionary says so — the application class carries an `unsaved`\n * property, and `save` is documented as \"Save ALL Contacts changes\".\n *\n * Two consequences, both load-bearing:\n *\n * 1. **A write that forgets `save()` silently does nothing.** The object updates,\n * every read-back inside the same script agrees, and the store never changes.\n * Every script below saves before it verifies.\n * 2. **`save()` is not scoped to our change.** It commits whatever else is\n * pending, including an edit someone has half-typed in the Contacts window.\n * That is a property of the dictionary, not a choice made here, and the tool\n * descriptions say so.\n */\n\n/**\n * Shared prelude.\n *\n * The bundle identifier is `com.apple.AddressBook`, which — like Calendar's\n * `com.apple.iCal` — does not match the display name. Contacts.app kept the id\n * it shipped with as Address Book. `Application(\"Contacts\")` is the correct\n * scripting name; `com.apple.Contacts` does not exist.\n */\nexport const PRELUDE = `\nObjC.import(\"AppKit\");\n\nfunction isContactsRunning() {\n var apps = $.NSRunningApplication.runningApplicationsWithBundleIdentifier(\"com.apple.AddressBook\");\n return apps.count > 0;\n}\n\nfunction ok(data) { return JSON.stringify({ ok: true, data: data }); }\nfunction err(code, message) { return JSON.stringify({ ok: false, error: { code: code, message: String(message) } }); }\n\n/** Read one property defensively: Contacts throws on properties it cannot supply. */\nfunction prop(fn, fallback) {\n try {\n var v = fn();\n return v === undefined ? fallback : v;\n } catch (e) {\n return fallback;\n }\n}\n\n/**\n * Find one person, without assuming the two lanes spell an id the same way.\n *\n * The file lane holds \\`ZABCDRECORD.ZUNIQUEID\\`; Apple Events returns whatever\n * \\`person.id()\\` returns. That those are the same string is EXACTLY the kind of\n * thing this project has been wrong about before — Calendar's \\`calendar.uid()\\`\n * throws for every calendar, and the id bridge that held for its events did not\n * extend to them. It is unmeasured here, so it is not assumed.\n *\n * Two attempts, cheapest first:\n *\n * 1. \\`byId()\\` with the value as given. Contacts offers a real by-id lookup,\n * unlike Calendar, so when the forms do agree this costs one round trip.\n * 2. A bulk \\`people.id()\\` fetch, matched on the UUID substring. This is the\n * guard, and it is affordable precisely here: docs/contacts.md measured the\n * whole id list at 63-73 ms over 421 people, against the 1.8 s that made the\n * same trick unusable for Calendar's events.\n *\n * So a mismatch in id FORM degrades to a fast scan instead of a wrong \"not\n * found\" — which is the failure this would otherwise produce, and the one that\n * looks like the contact was deleted.\n */\nfunction uuidOf(value) {\n var m = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/.exec(String(value || \"\"));\n return m ? m[0].toUpperCase() : null;\n}\n\nfunction findPerson(C, personId) {\n try {\n var direct = C.people.byId(personId);\n direct.id();\n return direct;\n } catch (e) {\n // Fall through to the scan.\n }\n\n var wanted = uuidOf(personId);\n if (!wanted) return null;\n\n try {\n var ids = C.people.id();\n for (var i = 0; i < ids.length; i++) {\n if (uuidOf(ids[i]) === wanted) {\n var found = C.people.byId(ids[i]);\n found.id();\n return found;\n }\n }\n } catch (e2) {\n return null;\n }\n return null;\n}\n\n/** Everything a write returns, so a caller sees what Contacts stored. */\nfunction shapePerson(p) {\n return {\n id: prop(function () { return String(p.id()); }, null),\n name: prop(function () { return p.name(); }, null),\n firstName: prop(function () { return p.firstName(); }, null),\n lastName: prop(function () { return p.lastName(); }, null),\n nickname: prop(function () { return p.nickname(); }, null),\n organization: prop(function () { return p.organization(); }, null),\n jobTitle: prop(function () { return p.jobTitle(); }, null),\n department: prop(function () { return p.department(); }, null),\n note: prop(function () { return p.note(); }, null),\n company: prop(function () { return p.company(); }, false),\n phones: prop(function () {\n var out = [];\n var xs = p.phones();\n for (var i = 0; i < xs.length; i++) {\n out.push({\n label: prop(function () { return xs[i].label(); }, null),\n value: prop(function () { return xs[i].value(); }, null)\n });\n }\n return out;\n }, []),\n emails: prop(function () {\n var out = [];\n var xs = p.emails();\n for (var i = 0; i < xs.length; i++) {\n out.push({\n label: prop(function () { return xs[i].label(); }, null),\n value: prop(function () { return xs[i].value(); }, null)\n });\n }\n return out;\n }, [])\n };\n}\n\n/**\n * Apply the scalar properties a caller supplied.\n *\n * Only keys actually present are touched: a missing key means \"leave it alone\",\n * an explicit null means \"clear it\". Assigning undefined would blank a field the\n * caller never mentioned.\n */\nfunction applyFields(p, f) {\n if (f.firstName !== undefined) p.firstName = f.firstName;\n if (f.lastName !== undefined) p.lastName = f.lastName;\n if (f.nickname !== undefined) p.nickname = f.nickname;\n if (f.organization !== undefined) p.organization = f.organization;\n if (f.jobTitle !== undefined) p.jobTitle = f.jobTitle;\n if (f.department !== undefined) p.department = f.department;\n if (f.note !== undefined) p.note = f.note;\n if (f.company !== undefined) p.company = f.company;\n}\n\n/**\n * Add phone and email elements.\n *\n * These are ELEMENTS, not properties: a phone number is its own object made at\n * the end of the person's phones. There is no way to set them as a bulk array,\n * so each one is a separate \\`make\\`.\n */\nfunction addChildren(C, p, phones, emails) {\n var i;\n if (phones) {\n for (i = 0; i < phones.length; i++) {\n p.phones.push(C.Phone({ label: phones[i].label || \"mobile\", value: phones[i].value }));\n }\n }\n if (emails) {\n for (i = 0; i < emails.length; i++) {\n p.emails.push(C.Email({ label: emails[i].label || \"home\", value: emails[i].value }));\n }\n }\n}\n`;\n","import { PRELUDE } from \"./core.js\";\n\n/**\n * The write scripts. Two verbs, and the absence of a third is deliberate.\n *\n * `create` and `update` are both `make`-and-`save`. There is no `delete`,\n * because the Contacts dictionary has no delete command — see `core.ts` for the\n * measurement. That absence is enforced by `test/jxa.test.ts`, so removing a\n * contact cannot be added here without the decision being taken again.\n *\n * Every script SAVES and then RE-READS, in that order. Contacts keeps changes in\n * an unsaved buffer, so a script that skips the save mutates a live object,\n * reports success from its own in-memory read, and leaves the store untouched.\n * Verifying before saving would find exactly the same false success.\n */\n\nexport const CREATE_CONTACT = `${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0]);\n var C = Application(\"Contacts\");\n\n if (!isContactsRunning() && !p.allowLaunch) {\n return err(\"APP_NOT_RUNNING\", \"Contacts is not running.\");\n }\n\n var person;\n try {\n person = C.Person({\n firstName: p.fields.firstName || \"\",\n lastName: p.fields.lastName || \"\"\n });\n C.people.push(person);\n } catch (e) {\n return err(\"CREATE_FAILED\", e.message || e);\n }\n\n try {\n applyFields(person, p.fields);\n addChildren(C, person, p.phones, p.emails);\n } catch (e) {\n // The person exists but is incomplete. Save anyway so the caller is told\n // about a real half-written card rather than a phantom one, and let the\n // read-back below show exactly what landed.\n try { C.save(); } catch (e2) {}\n return err(\"CREATE_INCOMPLETE\", e.message || e);\n }\n\n try {\n C.save();\n } catch (e) {\n return err(\"SAVE_FAILED\", e.message || e);\n }\n\n // Re-read AFTER the save, so what comes back is what Contacts stored rather\n // than what this script asked for.\n var fresh = findPerson(C, prop(function () { return String(person.id()); }, \"\"));\n if (!fresh) {\n return err(\"CREATE_NOT_PERSISTED\", \"Contacts saved without error but the contact could not be read back.\");\n }\n return ok(shapePerson(fresh));\n}\n`;\n\nexport const UPDATE_CONTACT = `${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0]);\n var C = Application(\"Contacts\");\n\n if (!isContactsRunning() && !p.allowLaunch) {\n return err(\"APP_NOT_RUNNING\", \"Contacts is not running.\");\n }\n\n var person = findPerson(C, p.personId);\n if (!person) {\n return err(\"CONTACT_NOT_FOUND\", \"No contact with id \" + p.personId + \".\");\n }\n\n try {\n applyFields(person, p.fields);\n addChildren(C, person, p.phones, p.emails);\n } catch (e) {\n return err(\"UPDATE_FAILED\", e.message || e);\n }\n\n try {\n C.save();\n } catch (e) {\n return err(\"SAVE_FAILED\", e.message || e);\n }\n\n var fresh = findPerson(C, p.personId);\n if (!fresh) {\n return err(\"UPDATE_NOT_PERSISTED\", \"Contacts saved without error but the contact could not be read back.\");\n }\n return ok(shapePerson(fresh));\n}\n`;\n","import { readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { describeStore, type StoreFacts } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Contacts' stores — plural, which is the whole point of this file.\n *\n * Every other surface in this repo has one store. Contacts has one per account\n * plus a root database, and `docs/contacts.md` measured what that means:\n *\n * AddressBook-v22.abcddb 1 contact\n * Sources/<uuid>/AddressBook-v22.abcddb 420 contacts\n *\n * The obvious path — the one at the top of the directory — is present, readable,\n * correctly shaped, and empty. A server that opens it gets a working database\n * with nobody in it, which fails no check and returns no answer. That is not a\n * hypothetical: `scripts/probe-contacts.mjs` did exactly this and reported a\n * confident 0% resolution rate before anyone noticed.\n *\n * So there is no \"the\" store here. Everything readable is opened and the rows\n * are unioned, and the number of sources is discovered rather than assumed —\n * one on the probed machine, more with Google or Exchange accounts.\n */\n\n/** `~/Library/Application Support/AddressBook`. */\nexport const ADDRESSBOOK_DIR = join(\"Library\", \"Application Support\", \"AddressBook\");\n\n/** The per-account subdirectory. Each child holds one database. */\nexport const SOURCES_DIRNAME = \"Sources\";\n\n/** Constant on every store, root and source alike. */\nexport const STORE_FILENAME = \"AddressBook-v22.abcddb\";\n\nexport type StoreCandidate = StoreFacts & {\n path: string;\n /** `root` for the top-level database, else the source directory name. */\n label: string;\n};\n\nexport type LocateResult = {\n dirPath: string;\n dirListable: boolean;\n /** Every store-shaped file found, root first. */\n candidates: StoreCandidate[];\n /** The subset that can actually be opened. May be empty. */\n readable: StoreCandidate[];\n /** How many `Sources/*` directories were seen, readable or not. */\n sourceCount: number;\n reason: string | null;\n};\n\nexport const defaultDirPath = (home: string = homedir()): string => join(home, ADDRESSBOOK_DIR);\n\n/**\n * The grant hint.\n *\n * Deliberately NOT the Full Disk Access sentence the other surfaces use.\n * Contacts is protected by its own TCC service, and unlike Full Disk Access that\n * one prompts — so the likely fix is a dialog that was dismissed, and the\n * remedy names the Contacts pane rather than asking for the whole disk.\n */\nconst GRANT_HINT =\n \"Contacts is protected by its own privacy permission, not by Full Disk Access. macOS asks for \" +\n \"it the first time something reads the address book; if that dialog was dismissed, re-enable \" +\n \"the app under System Settings > Privacy & Security > Contacts and restart it.\";\n\nconst listDirs = (dir: string): string[] => {\n try {\n return readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && !e.name.startsWith(\".\"))\n .map((e) => e.name);\n } catch {\n return [];\n }\n};\n\nexport const locateStores = (\n opts: { storePath?: string | undefined; home?: string } = {},\n): LocateResult => {\n const dirPath = defaultDirPath(opts.home);\n\n // An explicit path is a bypass, for tests and forensic copies. No discovery,\n // and no union — the caller said which file it meant.\n if (opts.storePath) {\n const candidate: StoreCandidate = {\n ...describeStore(opts.storePath),\n path: opts.storePath,\n label: \"explicit\",\n };\n return {\n dirPath,\n dirListable: true,\n candidates: [candidate],\n readable: candidate.readable ? [candidate] : [],\n sourceCount: 0,\n reason: candidate.readable\n ? null\n : candidate.exists\n ? `The store at ${opts.storePath} exists but cannot be read. ${GRANT_HINT}`\n : `No file at ${opts.storePath}. APPLE_CONTACTS_STORE points at nothing.`,\n };\n }\n\n const rootPath = join(dirPath, STORE_FILENAME);\n const sourceNames = listDirs(join(dirPath, SOURCES_DIRNAME));\n\n const candidates: StoreCandidate[] = [\n { ...describeStore(rootPath), path: rootPath, label: \"root\" },\n ...sourceNames.map((name) => {\n const path = join(dirPath, SOURCES_DIRNAME, name, STORE_FILENAME);\n return { ...describeStore(path), path, label: name };\n }),\n ].filter((c) => c.exists);\n\n const readable = candidates.filter((c) => c.readable);\n\n // `dirListable` is the signal here, not a store's readability: the root file\n // can be statted without the grant, so \"the file is there\" proves nothing. If\n // the directory cannot be listed then the sources cannot even be enumerated,\n // and the sources are where the contacts are.\n const dirListable = listDirs(dirPath).length > 0 || sourceNames.length > 0;\n\n const reason = readable.length\n ? null\n : candidates.length\n ? `Found ${candidates.length} Contacts store(s) under ${dirPath} but none could be opened. ${GRANT_HINT}`\n : dirListable\n ? `No ${STORE_FILENAME} under ${dirPath}. Has Contacts ever been set up on this account?`\n : `${dirPath} could not be listed, so the per-account stores could not be found. ${GRANT_HINT}`;\n\n return { dirPath, dirListable, candidates, readable, sourceCount: sourceNames.length, reason };\n};\n","/**\n * Phone number matching, which on this surface is the whole product.\n *\n * Contacts stores what the user typed. `docs/contacts.md` measured a 400-row\n * sample of `ZFULLNUMBER`: 222 formatted (`06 12 34 56 78`), 159 already E.164,\n * 15 bare digits. Messages, meanwhile, stores a handle as E.164 and nothing\n * else. So the two never meet as strings, and the measurement says so with an\n * unusually blunt number: **exact string equality resolves 3.7% of message\n * traffic.** A resolver that joins on the stored value is not slightly wrong, it\n * is useless.\n *\n * What works is a SUFFIX. No prefix rule connects `06…` to `+336…` without\n * knowing the user's country, which nothing here has any business guessing, but\n * the two agree from the ninth digit back.\n */\n\n/** Everything that is not a digit, removed. The base of every key below. */\nexport const digitsOf = (value: string): string => value.replaceAll(/\\D/g, \"\");\n\n/**\n * How many trailing digits make a key. Nine, measured rather than picked.\n *\n * | key | recent traffic resolved | ambiguous |\n * | -------- | ----------------------- | --------- |\n * | exact | 8.5% | 1 |\n * | 10 | 96.7% | 5 |\n * | **9** | **97.6%** | **6** |\n * | 7 | 97.6% | 6 |\n *\n * Seven ties nine on every column measured, so nine wins on the tie-break that\n * matters: a shorter key can only ever collide more. Ten is where French\n * national numbers (`0612345678`, ten digits) stop lining up with the same\n * number in E.164 (`+33612345678`, eleven) — which is exactly why 10 does no\n * better than plain digits and 9 does.\n */\nexport const SUFFIX_DIGITS = 9;\n\n/**\n * Below this, a number is a shortcode — a bank, a delivery service, a 2FA\n * sender. 115 of the 958 handles in the measured `chat.db` were these. They can\n * never resolve to a contact, and counting them as failures is how a resolver\n * ends up reporting a far worse rate than it earns.\n */\nconst SHORTCODE_MAX_DIGITS = 6;\n\nexport const isShortcode = (value: string): boolean => {\n const d = digitsOf(value);\n return d.length > 0 && d.length <= SHORTCODE_MAX_DIGITS;\n};\n\n/**\n * The lookup key, or `null` when the value is too short to make one.\n *\n * Returning `null` rather than a short key is deliberate: a three-digit key\n * would match any number ending in those digits, which is the failure mode this\n * whole module exists to avoid.\n */\nexport const suffixKey = (value: string, digits: number = SUFFIX_DIGITS): string | null => {\n const d = digitsOf(value);\n return d.length >= digits ? d.slice(-digits) : null;\n};\n\n/** Email keys are simply case-folded. Measured: 37 of 60 resolve, none ambiguous. */\nexport const emailKey = (value: string): string => value.trim().toLowerCase();\n\nexport type HandleKind = \"phone\" | \"email\" | \"shortcode\";\n\n/**\n * What kind of thing a Messages handle is.\n *\n * Order matters: `@` decides first, because an email address can contain digits\n * and a phone number can never contain an `@`.\n */\nexport const handleKind = (handle: string): HandleKind => {\n if (handle.includes(\"@\")) return \"email\";\n return isShortcode(handle) ? \"shortcode\" : \"phone\";\n};\n","import { emailKey, handleKind, suffixKey, type HandleKind } from \"./phone.js\";\nimport type { HandleLookup, IndexContact } from \"./store.js\";\n\n/**\n * Turn Messages handles into names.\n *\n * This is the function `packages/messages` exists to call, and the reason\n * Contacts was probed at all: `chat.db` records a correspondent as\n * `+15551234567` and nothing else, so a Messages server without this answers\n * \"+15551234567 said …\", which is complete and useless.\n *\n * ## What the measurement says it must do\n *\n * `docs/contacts.md` measured resolution against a real 958-handle store two\n * ways, and the gap between them is the whole design:\n *\n * | denominator | resolved |\n * | -------------------------- | -------- |\n * | every handle ever seen | 27.6% |\n * | messages in the last year | 97.6% |\n * | the 25 busiest correspondents | 84% |\n *\n * The first number is a fact about the address book — 321 handles sent exactly\n * one message, ever — not about this resolver. The last one is the one that\n * shapes the API: **about one in six of the busiest correspondents does not\n * resolve.** So `unknown` is a normal, expected, first-class outcome. It is not\n * an error, it must not throw, and a caller that treats it as a failure will be\n * wrong several times on any real inbox.\n */\n\nexport type ResolutionStatus =\n /** Exactly one contact carries this handle. */\n | \"resolved\"\n /** Nobody does. Normal — see above. */\n | \"unknown\"\n /**\n * More than one distinct contact does.\n *\n * Reported rather than resolved by picking a winner. Six handles collided at\n * nine digits on the probed store, and the failure mode of guessing is putting\n * one person's name on another person's messages — which is worse than no name\n * at all, because it is not visibly wrong.\n */\n | \"ambiguous\"\n /** A shortcode: a bank, a courier, a 2FA sender. Can never be a contact. */\n | \"shortcode\";\n\nexport type ResolvedHandle = {\n handle: string;\n kind: HandleKind;\n status: ResolutionStatus;\n /** The name to show. Null unless `status` is `resolved`. */\n name: string | null;\n contact: IndexContact | null;\n /** How many distinct contacts matched. 0, 1, or more. */\n matches: number;\n};\n\n/**\n * Distinct PEOPLE, not distinct rows.\n *\n * A contact with the same number stored twice (mobile and iPhone, which Contacts\n * does routinely) would otherwise read as ambiguous. Linked records across two\n * accounts are collapsed on `ZLINKID` for the same reason: Contacts shows one\n * unified card for them, and reporting two names for one person would contradict\n * what the user sees in the app.\n */\nconst distinctPeople = (ids: Iterable<string>, lookup: HandleLookup): IndexContact[] => {\n const seen = new Map<string, IndexContact>();\n for (const id of ids) {\n const contact = lookup.contacts.get(id);\n if (!contact) continue;\n // Prefer the link id so cross-account duplicates fold together; fall back to\n // the record's own identity when it carries no link.\n const key = contact.linkId === null ? `pk:${id}` : `link:${contact.linkId}`;\n if (!seen.has(key)) seen.set(key, contact);\n }\n return [...seen.values()];\n};\n\nexport const resolveHandle = (handle: string, lookup: HandleLookup): ResolvedHandle => {\n const kind = handleKind(handle);\n const base = { handle, kind, name: null, contact: null, matches: 0 } as const;\n\n if (kind === \"shortcode\") return { ...base, status: \"shortcode\" };\n\n const ids =\n kind === \"email\"\n ? lookup.byEmail.get(emailKey(handle))\n : (() => {\n const key = suffixKey(handle, lookup.suffixDigits);\n return key === null ? undefined : lookup.byPhone.get(key);\n })();\n\n if (!ids?.size) return { ...base, status: \"unknown\" };\n\n const people = distinctPeople(ids, lookup);\n if (people.length === 1) {\n const contact = people[0]!;\n return { handle, kind, status: \"resolved\", name: contact.displayName, contact, matches: 1 };\n }\n if (people.length === 0) return { ...base, status: \"unknown\" };\n return { ...base, status: \"ambiguous\", matches: people.length };\n};\n\nexport const resolveHandles = (\n handles: readonly string[],\n lookup: HandleLookup,\n): ResolvedHandle[] => handles.map((h) => resolveHandle(h, lookup));\n\n/** Counts by status, for a caller that wants to report coverage honestly. */\nexport const summarise = (results: readonly ResolvedHandle[]): Record<ResolutionStatus, number> => {\n const out: Record<ResolutionStatus, number> = {\n resolved: 0,\n unknown: 0,\n ambiguous: 0,\n shortcode: 0,\n };\n for (const r of results) out[r.status] += 1;\n return out;\n};\n","import type { DatabaseSync } from \"node:sqlite\";\n\nimport {\n columnsOf,\n CORE_DATA_EPOCH_OFFSET,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n SchemaDriftError,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { digitsOf, emailKey, suffixKey, SUFFIX_DIGITS } from \"./phone.js\";\n\n/**\n * Contacts' file lane.\n *\n * Reads only, and there is no Apple Events lane at all — not even a fallback.\n * `docs/contacts.md` measured the dictionary as fast (63 ms for every id, 52 ms\n * for every phone number) and still ruled it out for reads, because the thing\n * this surface exists to do is join 970 stored numbers against a set of handles\n * by suffix, and no amount of round trips gets a keyed index out of `osascript`.\n * The consequence is worth stating: **a read-only surface needs no Automation\n * grant**, so this server never prompts for one.\n *\n * ## Two things measured here that are not obvious from the schema\n *\n * **The store is plural.** See `locate.ts`. Every method on `ContactsIndex` fans\n * out over shards and merges.\n *\n * **`ZABCDRECORD` is not a table of contacts.** It is a Core Data single-table\n * inheritance root, and groups, containers and an info row live in it alongside\n * people — 425 rows for 420 contacts on the probed machine. `Z_ENT` is the only\n * discriminator, and it is resolved through `Z_PRIMARYKEY` BY NAME rather than\n * hardcoded, because Core Data assigns those numbers per model version.\n */\n\n/** Tables the lane cannot work without. */\nconst REQUIRED = [\"ZABCDRECORD\"] as const;\n\n/** The schema this was written against. Named in the drift error, not enforced. */\nconst PROBED_FINGERPRINT = \"4f2871e93f6b\";\nconst PROBED_MACOS = \"26.6\";\n\n/**\n * Entity names that mean \"a person\".\n *\n * `ABCDSubscribedContact` inherits from `ABCDContact` and is included: a contact\n * arriving from a subscribed source is still someone whose name should appear\n * beside their messages. Groups (`ABCDGroup`, `ABCDSmartGroup`) and the\n * bookkeeping entities (`ABCDInfo`, `CNCDContainer`) are not people.\n */\nconst CONTACT_ENTITIES = /^(ABCD)?(Subscribed)?Contact$/i;\n\nexport type StoreCapabilities = {\n fingerprint: string;\n recordColumns: Set<string>;\n phoneColumns: Set<string>;\n emailColumns: Set<string>;\n /** `Z_ENT` values that mean a person. Empty means the filter could not be built. */\n contactEntities: number[];\n hasPhones: boolean;\n hasEmails: boolean;\n hasNotes: boolean;\n epochOffset: number;\n};\n\nexport type IndexContact = {\n recordPk: number;\n /** Stable across runs; the ref is built from it. */\n uniqueId: string | null;\n firstName: string | null;\n lastName: string | null;\n nickname: string | null;\n organization: string | null;\n jobTitle: string | null;\n /** Assembled below — never a raw column, because no single column holds it. */\n displayName: string;\n /** Which store this came from, so a duplicate across accounts is explicable. */\n source: string;\n linkId: number | null;\n isMe: boolean;\n};\n\nexport type ContactPhone = { recordPk: number; value: string; label: string | null };\nexport type ContactEmail = { recordPk: number; value: string; label: string | null };\n\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\nconst text = (v: unknown): string | null => (typeof v === \"string\" && v.length > 0 ? v : null);\n\n/**\n * A name to show, assembled from whatever the record actually carries.\n *\n * Falls through deliberately: plenty of real contacts are an organisation with\n * no person name (a garage, a doctor's office), and plenty are a first name\n * alone. Returning an empty string would put a blank where a sender should be,\n * so the last resort is explicit.\n */\nexport const displayNameOf = (c: {\n firstName: string | null;\n lastName: string | null;\n nickname: string | null;\n organization: string | null;\n}): string => {\n const full = [c.firstName, c.lastName].filter(Boolean).join(\" \").trim();\n return full || c.nickname || c.organization || \"(no name)\";\n};\n\n/**\n * Add one key to one bucket. A null key is skipped, never stored as `\"\"` — a\n * number too short to make a key must not become a key that matches everything.\n */\nconst remember = (map: Map<string, Set<string>>, key: string | null, id: string): void => {\n if (!key) return;\n const bucket = map.get(key);\n if (bucket) bucket.add(id);\n else map.set(key, new Set([id]));\n};\n\n/** One opened database, with what was learned about it. */\nexport type Shard = {\n db: DatabaseSync;\n mode: string;\n caps: StoreCapabilities;\n path: string;\n label: string;\n contacts: number;\n};\n\nexport class ContactsIndex {\n readonly shards: readonly Shard[];\n\n constructor(shards: readonly Shard[]) {\n this.shards = shards;\n }\n\n /** Every shard's fingerprint. More than one distinct value is worth showing. */\n get fingerprints(): string[] {\n return [...new Set(this.shards.map((s) => s.caps.fingerprint))];\n }\n\n get totalContacts(): number {\n return this.shards.reduce((n, s) => n + s.contacts, 0);\n }\n\n /**\n * Project a column, or a typed NULL when this store does not have it.\n *\n * Same guard as the other surfaces: the schema is reverse-engineered and\n * unversioned, so an Apple rename costs one field rather than the lane.\n */\n static #col(present: Set<string>, table: string, name: string, alias: string): string {\n return present.has(name) ? `${table}.\"${name}\" AS ${alias}` : `NULL AS ${alias}`;\n }\n\n static #entFilter(caps: StoreCapabilities, alias: string): string {\n if (!caps.contactEntities.length) return \"\";\n return `WHERE ${alias}.\"Z_ENT\" IN (${caps.contactEntities.join(\", \")})`;\n }\n\n #contactsFrom(shard: Shard, where: string, params: unknown[], limit: number): IndexContact[] {\n const c = shard.caps.recordColumns;\n const col = ContactsIndex.#col;\n const ent = ContactsIndex.#entFilter(shard.caps, \"r\");\n const extra = where ? `${ent ? \"AND\" : \"WHERE\"} ${where}` : \"\";\n const sql = `\n SELECT r.\"Z_PK\" AS recordPk,\n ${col(c, \"r\", \"ZUNIQUEID\", \"uniqueId\")},\n ${col(c, \"r\", \"ZFIRSTNAME\", \"firstName\")},\n ${col(c, \"r\", \"ZLASTNAME\", \"lastName\")},\n ${col(c, \"r\", \"ZNICKNAME\", \"nickname\")},\n ${col(c, \"r\", \"ZORGANIZATION\", \"organization\")},\n ${col(c, \"r\", \"ZJOBTITLE\", \"jobTitle\")},\n ${col(c, \"r\", \"ZLINKID\", \"linkId\")},\n ${col(c, \"r\", \"ZCONTAINERWHERECONTACTISME\", \"isMe\")}\n FROM \"ZABCDRECORD\" r\n ${ent} ${extra}\n ORDER BY r.\"ZLASTNAME\" ASC, r.\"ZFIRSTNAME\" ASC\n LIMIT ${Math.max(1, Math.trunc(limit))}`;\n const rows = shard.db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[];\n return rows.map((r) => {\n const parts = {\n firstName: text(r.firstName),\n lastName: text(r.lastName),\n nickname: text(r.nickname),\n organization: text(r.organization),\n };\n return {\n recordPk: Number(r.recordPk),\n uniqueId: text(r.uniqueId),\n ...parts,\n jobTitle: text(r.jobTitle),\n displayName: displayNameOf(parts),\n source: shard.label,\n linkId: num(r.linkId),\n isMe: num(r.isMe) !== null,\n };\n });\n }\n\n /** Every contact, across every shard. */\n list(limit: number): IndexContact[] {\n return this.shards.flatMap((s) => this.#contactsFrom(s, \"\", [], limit)).slice(0, limit);\n }\n\n /**\n * Name search.\n *\n * `LIKE ? ESCAPE '\\'` with core's `escapeLike`, so a contact called \"100%\n * Design\" can be searched for literally instead of matching everyone.\n */\n search(query: string, limit: number): IndexContact[] {\n const needle = `%${escapeLike(query)}%`;\n const out: IndexContact[] = [];\n for (const shard of this.shards) {\n const c = shard.caps.recordColumns;\n const fields = [\"ZFIRSTNAME\", \"ZLASTNAME\", \"ZNICKNAME\", \"ORGANIZATION\", \"ZORGANIZATION\"]\n .filter((f) => c.has(f))\n .map((f) => `r.\"${f}\" LIKE ? ESCAPE '\\\\'`);\n if (!fields.length) continue;\n out.push(\n ...this.#contactsFrom(\n shard,\n `(${fields.join(\" OR \")})`,\n fields.map(() => needle),\n limit,\n ),\n );\n }\n return out.slice(0, limit);\n }\n\n byPk(shardLabel: string, recordPk: number): IndexContact | null {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return null;\n return this.#contactsFrom(shard, `r.\"Z_PK\" = ?`, [recordPk], 1)[0] ?? null;\n }\n\n #childRows(\n shard: Shard,\n table: string,\n caps: Set<string>,\n valueColumns: readonly string[],\n recordPks?: readonly number[],\n ): { recordPk: number; value: string; label: string | null }[] {\n if (!caps.size) return [];\n const valueCol = valueColumns.find((v) => caps.has(v));\n if (!valueCol || !caps.has(\"ZOWNER\")) return [];\n const scope = recordPks?.length\n ? `AND x.\"ZOWNER\" IN (${recordPks.map(() => \"?\").join(\", \")})`\n : \"\";\n const sql = `\n SELECT x.\"ZOWNER\" AS recordPk,\n x.\"${valueCol}\" AS value,\n ${caps.has(\"ZLABEL\") ? `x.\"ZLABEL\"` : \"NULL\"} AS label\n FROM \"${table}\" x\n WHERE x.\"${valueCol}\" IS NOT NULL AND x.\"${valueCol}\" <> '' ${scope}`;\n const rows = shard.db.prepare(sql).all(...((recordPks ?? []) as never[])) as Record<\n string,\n unknown\n >[];\n return rows.flatMap((r) => {\n const value = text(r.value);\n const recordPk = num(r.recordPk);\n if (!value || recordPk === null) return [];\n return [{ recordPk, value, label: text(r.label) }];\n });\n }\n\n phonesFor(shardLabel: string, recordPks: readonly number[]): ContactPhone[] {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return [];\n return this.#childRows(\n shard,\n \"ZABCDPHONENUMBER\",\n shard.caps.phoneColumns,\n [\"ZFULLNUMBER\"],\n recordPks,\n );\n }\n\n emailsFor(shardLabel: string, recordPks: readonly number[]): ContactEmail[] {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return [];\n return this.#childRows(\n shard,\n \"ZABCDEMAILADDRESS\",\n shard.caps.emailColumns,\n [\"ZADDRESS\", \"ZADDRESSNORMALIZED\"],\n recordPks,\n );\n }\n\n /**\n * The resolver index: every phone suffix and every email, keyed to a contact.\n *\n * Built in one pass over every shard because a handle does not know which\n * account its owner lives in. A key mapping to more than one DISTINCT contact\n * is kept as such — see `resolve.ts`, which reports ambiguity rather than\n * picking. `docs/contacts.md` measured six such collisions at nine digits, and\n * twenty-eight at four.\n */\n buildLookup(suffixDigits: number = SUFFIX_DIGITS): HandleLookup {\n const byPhone = new Map<string, Set<string>>();\n const byEmail = new Map<string, Set<string>>();\n const contacts = new Map<string, IndexContact>();\n\n for (const shard of this.shards) {\n const people = this.#contactsFrom(shard, \"\", [], Number.MAX_SAFE_INTEGER);\n const byPk = new Map(people.map((p) => [p.recordPk, p]));\n for (const p of people) contacts.set(`${shard.label}:${p.recordPk}`, p);\n\n for (const row of this.#childRows(shard, \"ZABCDPHONENUMBER\", shard.caps.phoneColumns, [\n \"ZFULLNUMBER\",\n ])) {\n if (!byPk.has(row.recordPk)) continue;\n remember(byPhone, suffixKey(row.value, suffixDigits), `${shard.label}:${row.recordPk}`);\n }\n for (const row of this.#childRows(shard, \"ZABCDEMAILADDRESS\", shard.caps.emailColumns, [\n \"ZADDRESS\",\n \"ZADDRESSNORMALIZED\",\n ])) {\n if (!byPk.has(row.recordPk)) continue;\n remember(byEmail, emailKey(row.value), `${shard.label}:${row.recordPk}`);\n }\n }\n\n // The key length travels WITH the index. Indexing at nine and querying at\n // seven would match nothing and look exactly like an empty address book.\n return { byPhone, byEmail, contacts, suffixDigits };\n }\n\n close(): void {\n for (const s of this.shards) {\n try {\n s.db.close();\n } catch {\n // Closing a database that already failed is not worth reporting.\n }\n }\n }\n}\n\nexport type HandleLookup = {\n /** Phone suffix → the contacts carrying it. Key length is `suffixDigits`. */\n byPhone: Map<string, Set<string>>;\n /** Case-folded address → the contacts carrying it. */\n byEmail: Map<string, Set<string>>;\n /** `\"<shard>:<pk>\"` → the contact. */\n contacts: Map<string, IndexContact>;\n /** How the phone keys were built. Queries MUST use the same length. */\n suffixDigits: number;\n};\n\nexport const introspect = (db: DatabaseSync): StoreCapabilities => {\n const recordColumns = new Set(columnsOf(db, \"ZABCDRECORD\"));\n\n for (const t of REQUIRED) {\n if (recordColumns.size === 0) {\n throw new SchemaDriftError(\n `This Contacts store has no ${t} table. It was probed on macOS ${PROBED_MACOS} with ` +\n `schema fingerprint ${PROBED_FINGERPRINT} (a PROBE fingerprint — compare it against ` +\n `another probe run, not against the one diagnostics reports); re-run ` +\n `\\`pnpm probe:contacts\\` to see what changed.`,\n );\n }\n }\n\n // By name, never by number. Core Data assigns Z_ENT per model version.\n let contactEntities: number[] = [];\n try {\n const rows = db.prepare(`SELECT Z_ENT AS ent, Z_NAME AS name FROM Z_PRIMARYKEY`).all() as {\n ent: number;\n name: string;\n }[];\n contactEntities = rows.filter((r) => CONTACT_ENTITIES.test(r.name)).map((r) => Number(r.ent));\n } catch {\n // No Z_PRIMARYKEY means no filter. Reported through `contactEntities` being\n // empty rather than guessed at — an unfiltered count is visibly too high,\n // whereas a wrong hardcoded number silently returns the wrong people.\n contactEntities = [];\n }\n\n const phoneColumns = new Set(columnsOf(db, \"ZABCDPHONENUMBER\"));\n const emailColumns = new Set(columnsOf(db, \"ZABCDEMAILADDRESS\"));\n\n return {\n fingerprint: fingerprintSchema(db),\n recordColumns,\n phoneColumns,\n emailColumns,\n contactEntities,\n hasPhones: phoneColumns.size > 0,\n hasEmails: emailColumns.size > 0,\n hasNotes: columnsOf(db, \"ZABCDNOTE\").length > 0,\n // Measured as apple-seconds, but taken from core rather than written out\n // again: being 31 years out is the classic Core Data date bug.\n epochOffset: CORE_DATA_EPOCH_OFFSET,\n };\n};\n\n/** Count the people in one opened shard, with the entity filter applied. */\nexport const countContacts = (db: DatabaseSync, caps: StoreCapabilities): number => {\n const where = caps.contactEntities.length\n ? `WHERE \"Z_ENT\" IN (${caps.contactEntities.join(\", \")})`\n : \"\";\n try {\n const row = db.prepare(`SELECT COUNT(*) AS c FROM \"ZABCDRECORD\" ${where}`).get() as {\n c: number;\n };\n return Number(row.c ?? 0);\n } catch {\n return 0;\n }\n};\n\nexport const openShard = (\n path: string,\n label: string,\n mode: ReadOnlyMode,\n logger?: Logger,\n): Shard | null => {\n try {\n const {\n db,\n mode: used,\n validated,\n } = openReadOnly<StoreCapabilities>(path, mode, {\n label: \"Contacts store\",\n envVar: \"APPLE_CONTACTS_INDEX_MODE\",\n validate: introspect,\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n logger?.debug?.(\n \"opened a Contacts store with immutable=1, which skips the write-ahead log — \" +\n \"very recent edits may be missing until Contacts checkpoints.\",\n ),\n });\n return { db, mode: used, caps: validated, path, label, contacts: countContacts(db, validated) };\n } catch (err) {\n if (err instanceof SchemaDriftError) throw err;\n // One unreadable shard is not a failed lane. The others still answer, and\n // the union is reported with the shard count so a short answer is visible.\n logger?.debug?.(`skipped Contacts store ${label}: ${String(err)}`);\n return null;\n }\n};\n\nexport { digitsOf };\n","import {\n createOsascriptRunner,\n IndexUnavailableError,\n withBusyRetry,\n type Logger,\n type OsascriptRunner,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport {\n CONTACTS_SURFACE,\n ContactNotFoundError,\n ContactWriteNotPersistedError,\n ContactsUnavailableError,\n} from \"./errors.js\";\nimport { CREATE_CONTACT, UPDATE_CONTACT } from \"./jxa/write.js\";\nimport { locateStores, type LocateResult } from \"./locate.js\";\nimport { resolveHandles, summarise, type ResolvedHandle } from \"./resolve.js\";\nimport {\n ContactsIndex,\n openShard,\n type HandleLookup,\n type IndexContact,\n type Shard,\n} from \"./store.js\";\n\n/**\n * Contacts' one lane, orchestrated.\n *\n * There is no lane *choice* here, which is what makes this the smallest client\n * in the repo: no Apple Events fallback, no write path, no cache TTL. What it\n * does own is the two things the store is awkward about — opening several\n * databases instead of one, and building the resolver index lazily, because\n * building it walks every contact and every phone row and most callers only\n * want to list a few names.\n */\n\nexport type CreateClientOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches real Contacts. */\n osascript?: OsascriptRunner;\n /** Injected by tests. */\n home?: string;\n};\n\n/** The scalar fields a write may set. Absent means \"leave alone\". */\nexport type ContactFields = {\n firstName?: string | null;\n lastName?: string | null;\n nickname?: string | null;\n organization?: string | null;\n jobTitle?: string | null;\n department?: string | null;\n note?: string | null;\n company?: boolean;\n};\n\nexport type LabelledValue = { label?: string; value: string };\n\n/** What a write reports: what Contacts stored, re-read after the save. */\nexport type WriteResult = {\n ref: string | null;\n personId: string | null;\n name: string | null;\n organization: string | null;\n phones: { label: string | null; value: string | null }[];\n emails: { label: string | null; value: string | null }[];\n source: \"apple-events\";\n};\n\nexport type LaneStatus = {\n located: LocateResult;\n /** One row per store that opened. */\n shards: { label: string; path: string; mode: string; contacts: number; fingerprint: string }[];\n totalContacts: number;\n indexMode: string;\n};\n\nexport type ContactDetail = IndexContact & {\n phones: { value: string; label: string | null }[];\n emails: { value: string; label: string | null }[];\n};\n\nexport class AppleContactsClient {\n readonly #config: Config;\n readonly #logger: Logger | undefined;\n readonly #home: string | undefined;\n\n readonly #runner: OsascriptRunner;\n\n #located: LocateResult | null = null;\n #index: ContactsIndex | null = null;\n #indexTried = false;\n #lookup: HandleLookup | null = null;\n\n constructor(opts: CreateClientOptions) {\n this.#config = opts.config;\n this.#logger = opts.logger;\n this.#home = opts.home;\n this.#runner =\n opts.osascript ??\n createOsascriptRunner({\n surface: CONTACTS_SURFACE,\n osascriptPath: opts.config.osascriptPath,\n timeoutMs: opts.config.osascriptTimeoutMs,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n }\n\n get config(): Config {\n return this.#config;\n }\n\n located(): LocateResult {\n this.#located ??= locateStores({\n storePath: this.#config.storePath,\n ...(this.#home ? { home: this.#home } : {}),\n });\n return this.#located;\n }\n\n /**\n * Open every readable store, once.\n *\n * `indexMode: \"off\"` is honoured as a hard no — it is what the test suite uses\n * so that a machine WITH the grant does not silently read the developer's own\n * address book and pass or fail on data nobody wrote.\n */\n index(): ContactsIndex | null {\n if (this.#indexTried) return this.#index;\n this.#indexTried = true;\n if (this.#config.indexMode === \"off\") return null;\n\n const located = this.located();\n const mode = this.#config.indexMode === \"auto\" ? \"ro\" : this.#config.indexMode;\n const shards: Shard[] = [];\n for (const candidate of located.readable) {\n const shard = openShard(candidate.path, candidate.label, mode, this.#logger);\n if (shard) shards.push(shard);\n }\n this.#index = shards.length ? new ContactsIndex(shards) : null;\n return this.#index;\n }\n\n /**\n * The index, or an error naming what is wrong.\n *\n * Never `[]`. An empty list is the answer to \"you have no contacts\", and this\n * surface has exactly one way to produce that answer wrongly — reading the\n * root store and finding one person in it. Callers get a reason instead.\n */\n #require(): ContactsIndex {\n const index = this.index();\n if (index) return index;\n if (this.#config.indexMode === \"off\") {\n throw new IndexUnavailableError(\n \"The Contacts index is disabled (APPLE_CONTACTS_INDEX_MODE=off). This server has no \" +\n \"other lane, so nothing can be read until it is re-enabled.\",\n );\n }\n throw new ContactsUnavailableError(\n this.located().reason ?? \"No readable Contacts store was found.\",\n );\n }\n\n list(limit?: number): IndexContact[] {\n return this.#require().list(limit ?? this.#config.maxResults);\n }\n\n search(query: string, limit?: number): IndexContact[] {\n return this.#require().search(query, limit ?? this.#config.maxResults);\n }\n\n /** One contact with its phone numbers and email addresses. */\n get(source: string, recordPk: number): ContactDetail | null {\n const index = this.#require();\n const contact = index.byPk(source, recordPk);\n if (!contact) return null;\n return {\n ...contact,\n phones: index.phonesFor(source, [recordPk]).map(({ value, label }) => ({ value, label })),\n emails: index.emailsFor(source, [recordPk]).map(({ value, label }) => ({ value, label })),\n };\n }\n\n /**\n * Built on first use and kept.\n *\n * Walking every contact and every phone row is cheap once (970 rows on the\n * probed store) and pointless per call. There is no TTL: this process does not\n * write to Contacts, and a server that has been running while the user edited\n * their address book is not the case worth optimising for. `diagnostics`\n * reports when it was built.\n */\n lookup(): HandleLookup {\n this.#lookup ??= this.#require().buildLookup(this.#config.phoneSuffixDigits);\n return this.#lookup;\n }\n\n /** The function `packages/messages` is meant to call. */\n resolve(handles: readonly string[]): {\n results: ResolvedHandle[];\n summary: Record<string, number>;\n } {\n const results = resolveHandles(handles, this.lookup());\n return { results, summary: summarise(results) };\n }\n\n // ─── writes ────────────────────────────────────────────────────────────────\n // Apple Events, always. The store is opened `PRAGMA query_only` because\n // Contacts owns it and reconciles it against iCloud, so writing to it would\n // corrupt sync state — the lane policy in docs/distribution.md, not a\n // preference. There is no delete: the dictionary has no such command.\n\n async #run<T>(scriptText: string, params: unknown): Promise<T> {\n try {\n return await withBusyRetry(() => this.#runner.run<T>(scriptText, params));\n } catch (err) {\n const code = (err as { details?: { code?: string } })?.details?.code;\n const message = err instanceof Error ? err.message : String(err);\n if (code === \"CONTACT_NOT_FOUND\") throw new ContactNotFoundError(message);\n if (code === \"CREATE_NOT_PERSISTED\" || code === \"UPDATE_NOT_PERSISTED\") {\n throw new ContactWriteNotPersistedError(message);\n }\n throw err;\n }\n }\n\n /**\n * Invalidate the read lane after a write.\n *\n * The index and the resolver lookup are both built once and kept, so a contact\n * created through Apple Events would otherwise stay invisible to `resolve` for\n * the life of the process — the exact \"wrote it, cannot find it\" confusion the\n * id bridge exists to prevent.\n */\n #invalidate(): void {\n this.#index?.close();\n this.#index = null;\n this.#lookup = null;\n this.#indexTried = false;\n }\n\n #shapeWrite(data: Record<string, unknown>): WriteResult {\n const personId = typeof data.id === \"string\" ? data.id : null;\n const shaped = (key: string) =>\n Array.isArray(data[key])\n ? (data[key] as Record<string, unknown>[]).map((r) => ({\n label: typeof r.label === \"string\" ? r.label : null,\n value: typeof r.value === \"string\" ? r.value : null,\n }))\n : [];\n return {\n // Best effort: the file-lane ref needs a shard and a rowid, and the write\n // lane knows neither. A caller that wants one searches again — which is\n // also the only way to be sure the new row reached the store.\n ref: null,\n personId,\n name: typeof data.name === \"string\" ? data.name : null,\n organization: typeof data.organization === \"string\" ? data.organization : null,\n phones: shaped(\"phones\"),\n emails: shaped(\"emails\"),\n source: \"apple-events\",\n };\n }\n\n async createContact(input: {\n fields: ContactFields;\n phones?: readonly LabelledValue[];\n emails?: readonly LabelledValue[];\n }): Promise<WriteResult> {\n const data = await this.#run<Record<string, unknown>>(CREATE_CONTACT, {\n fields: input.fields,\n phones: input.phones ?? [],\n emails: input.emails ?? [],\n allowLaunch: true,\n });\n this.#invalidate();\n return this.#shapeWrite(data);\n }\n\n async updateContact(input: {\n personId: string;\n fields: ContactFields;\n phones?: readonly LabelledValue[];\n emails?: readonly LabelledValue[];\n }): Promise<WriteResult> {\n const data = await this.#run<Record<string, unknown>>(UPDATE_CONTACT, {\n personId: input.personId,\n fields: input.fields,\n phones: input.phones ?? [],\n emails: input.emails ?? [],\n allowLaunch: true,\n });\n this.#invalidate();\n return this.#shapeWrite(data);\n }\n\n status(): LaneStatus {\n const index = this.index();\n return {\n located: this.located(),\n shards: (index?.shards ?? []).map((s) => ({\n label: s.label,\n path: s.path,\n mode: s.mode,\n contacts: s.contacts,\n fingerprint: s.caps.fingerprint,\n })),\n totalContacts: index?.totalContacts ?? 0,\n indexMode: this.#config.indexMode,\n };\n }\n\n close(): void {\n this.#index?.close();\n this.#index = null;\n this.#lookup = null;\n this.#indexTried = false;\n }\n}\n","import { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * `k1:<account>/<recordPk>` — an opaque handle for one contact.\n *\n * ## Why the account rides along\n *\n * Because the store is plural. A record's `Z_PK` is a rowid, and rowids are only\n * unique WITHIN one database — the root store and each account store number\n * their rows from 1 independently. A bare pk would therefore resolve to a\n * different person depending on which store happened to be read first, which is\n * the kind of bug that produces a plausible wrong answer rather than an error.\n *\n * ## Why not `ZUNIQUEID`\n *\n * It exists and is stable, but it is not what the child tables join on —\n * `ZABCDPHONENUMBER.ZOWNER` points at `Z_PK`. Carrying the pk means a `get`\n * needs no extra lookup, and the account prefix supplies the uniqueness the pk\n * lacks. `uniqueId` is still returned on results for callers that want a\n * durable identity across a re-index.\n *\n * ## Why `k1`\n *\n * `c1:` is Calendar's and `r1:` is Reminders'; `k1` is free and the version\n * prefix keeps a future scheme change additive rather than a silent\n * reinterpretation of refs already sitting in a conversation.\n */\n\nexport const REF_VERSION = \"k1\";\n\nexport type ContactRef = { source: string; recordPk: number };\n\n/**\n * An account label can contain almost anything — it is a directory name, and on\n * the probed machine a UUID — so the pk is anchored as the tail and the label is\n * whatever precedes the last `/`. Splitting on the FIRST separator would break\n * on any label containing one.\n */\nconst REF_PATTERN = /^k1:(.+)\\/(\\d+)$/;\n\nexport class InvalidContactRefError extends AppleAutomationError {\n override readonly name = \"InvalidContactRefError\";\n\n constructor(raw: string) {\n super(\n `\"${raw}\" is not a contact ref. Refs come from apple_contacts_search_contacts or ` +\n `apple_contacts_list_contacts and look like \"k1:<account>/<id>\" — they are opaque and ` +\n `must not be constructed by hand.` +\n (raw.startsWith(\"c1:\") || raw.startsWith(\"r1:\")\n ? ` That one belongs to another surface: \"c1:\" refs are Calendar events and \"r1:\" refs ` +\n `are Reminders.`\n : \"\"),\n { ref: raw },\n );\n }\n}\n\nexport const encodeRef = (source: string, recordPk: number): string =>\n `${REF_VERSION}:${source}/${recordPk}`;\n\nexport const decodeRef = (raw: string): ContactRef => {\n const m = REF_PATTERN.exec(raw.trim());\n if (!m) throw new InvalidContactRefError(raw);\n const recordPk = Number(m[2]);\n if (!Number.isSafeInteger(recordPk) || recordPk <= 0) throw new InvalidContactRefError(raw);\n return { source: m[1]!, recordPk };\n};\n","import {\n BaseConfigSchema,\n parseBool,\n parseConfig,\n parseIntOpt,\n trimmed,\n} from \"@mgcrea/mcp-apple-core\";\nimport { z } from \"zod\";\n\n/**\n * Configuration is environment-only — this server holds no secret at all, its\n * access is the macOS permission the user granted.\n *\n * Note what is ABSENT, and why:\n *\n * - **No `allowWrites` behaviour.** It is inherited from `BaseConfigSchema` and\n * deliberately ignored: this surface registers no mutating tool, so there is\n * nothing for the flag to gate. Editing someone's address book from a tool\n * call was never part of what Contacts was probed for.\n * - **No `osascript` settings in use.** Also inherited, also unused — there is\n * no Apple Events lane here at all, which is what lets this server run without\n * an Automation grant.\n * - **No account allowlist.** Contacts are unioned across accounts precisely so\n * that a handle resolves wherever its owner lives; scoping that by account\n * would reintroduce the bug this surface exists to avoid.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /**\n * Explicit store path. Bypasses discovery — for tests and forensic copies.\n *\n * Naming one file also DISABLES the union, which is the point of having it:\n * a test needs a single known database, not whatever the machine happens to\n * hold.\n */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\n /**\n * Trailing digits that make a phone key.\n *\n * Exposed because the right value is a fact about the user's country, and nine\n * was measured on one machine with mostly French and E.164 numbers. Lower is\n * more forgiving and collides more; the ambiguity count in `diagnostics` is\n * how to tell whether a change helped.\n */\n phoneSuffixDigits: z.number().int().min(6).max(15).default(9),\n}).strict();\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\nexport const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config =>\n parseConfig(ConfigSchema, {\n allowWrites: parseBool(env.APPLE_CONTACTS_ALLOW_WRITES),\n exposePrompts: parseBool(env.APPLE_CONTACTS_EXPOSE_PROMPTS),\n debug: parseBool(env.APPLE_CONTACTS_DEBUG),\n storePath: trimmed(env.APPLE_CONTACTS_STORE),\n indexMode: trimmed(env.APPLE_CONTACTS_INDEX_MODE),\n phoneSuffixDigits: parseIntOpt(env.APPLE_CONTACTS_PHONE_SUFFIX_DIGITS),\n osascriptPath: trimmed(env.APPLE_CONTACTS_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_CONTACTS_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_CONTACTS_MAX_RESULTS),\n });\n","/**\n * The Contacts operating manual, served as `cupertino://contacts/guide` and\n * embedded ahead of every Contacts prompt. Static by design — see the note in\n * the Mail guide.\n */\nexport const CONTACTS_GUIDE = `# Apple Contacts — how to drive this server\n\n## This server is read-only by construction\n\nIt cannot create, edit or delete a contact, and enabling writes does not add a\ntool. If someone asks you to update a contact, say that plainly rather than\nlooking for a tool that is not there.\n\n## Which tool, under which constraint\n\n- **A raw phone number or email address** — from a Messages handle, a caller ID,\n a mail header — is \\`apple_contacts_resolve_handles\\`. Give it the identifiers\n as they came; it batches and returns one result per handle.\n- **A name, or part of one** is \\`apple_contacts_search_contacts\\`.\n- **\\`apple_contacts_get_contact\\`** returns one card in full.\n\nPhone matching is by trailing digits, because Contacts stores numbers as typed\n(\"06 12 34 56 78\") while most systems hand you E.164 (\"+33612345678\"). Exact\nstring matching resolves almost nothing and is not used.\n\n## Read \\`status\\`, never just \\`name\\`\n\nEvery resolve result carries a status, and three of the four are not a name:\n\n- **\\`resolved\\`** — exactly one contact. \\`name\\` is set.\n- **\\`unknown\\`** — nobody in the address book has this handle. **Common and not\n an error**: measured on a real store, about one in six of even the busiest\n correspondents does not resolve. Show the raw handle.\n- **\\`ambiguous\\`** — more than one contact has it, so **no name is returned**.\n Do not guess, and do not pick the first; \\`matches\\` says how many. Putting the\n wrong name on a message is worse than putting none, because it does not look\n wrong.\n- **\\`shortcode\\`** — a bank, a courier, a 2FA sender. Can never be a contact.\n\n## Permissions and shape\n\nContacts has its **own** privacy permission and is not covered by Full Disk\nAccess — and unlike Full Disk Access, macOS prompts for it. A store that cannot\nbe opened usually means that prompt was dismissed: System Settings > Privacy &\nSecurity > Contacts.\n\nThe address book is several databases, one per account. All readable ones are\nunioned; if diagnostics reports fewer opened than found, some accounts are\nmissing from every answer. A contact held in two accounts is folded onto its\nlink id, matching the single card Contacts.app shows.\n`;\n","import {\n registerWorkflowPrompt,\n requiredPromptArg,\n type PromptContext,\n} from \"@mgcrea/mcp-apple-core\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { CONTACTS_GUIDE } from \"./guide.js\";\n\nconst CTX: PromptContext = { surface: \"contacts\", guide: CONTACTS_GUIDE };\n\n/**\n * Contacts' one workflow prompt.\n *\n * There is one because there is one hard thing on this surface, and it is not\n * looking a name up — it is refusing to. An ambiguous handle returns no name on\n * purpose, and the failure this prompt exists to prevent is a model helpfully\n * choosing the first of three matches and presenting it as fact.\n */\nexport const registerPrompts = (server: McpServer): void => {\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_contacts_who_is\",\n title: \"Who is this\",\n description:\n \"Identify a person from a name, a phone number or an email address, reporting honestly \" +\n \"when the address book cannot say. Read-only.\",\n argsSchema: {\n who: requiredPromptArg(\n 'A name, phone number or email address — however it arrived, e.g. \"+33612345678\".',\n ),\n },\n build: ({ who }) => `Who is: ${who}\n\n1. If that looks like a phone number or an email address, use\n \\`apple_contacts_resolve_handles\\` — pass it exactly as given, without\n reformatting it. If it looks like a name, use\n \\`apple_contacts_search_contacts\\`.\n2. **Read \\`status\\` before you read \\`name\\`.** Report each case as what it is:\n - \\`resolved\\` — give the name.\n - \\`unknown\\` — say the address book does not have this handle and show it\n raw. This is normal, not a failure, and not worth apologising for.\n - \\`ambiguous\\` — say how many people share it and that you will not guess.\n Offer to show the candidates. **Never pick one.** A confidently wrong name\n is the one outcome here that nobody catches.\n - \\`shortcode\\` — say it is an automated sender, not a person.\n3. On a resolved contact, \\`apple_contacts_get_contact\\` for the full card if the\n user wants more than the name.\n\nIf nothing resolves at all, check \\`cupertino://contacts/diagnostics\\` before\nconcluding the address book is empty — Contacts has its own permission, separate\nfrom Full Disk Access, and a dismissed prompt looks exactly like no contacts.`,\n });\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"../build-info.js\";\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * Build the report.\n *\n * Split out of the tool registration so the `cupertino://contacts/diagnostics`\n * resource can serve the same bytes. Two renderings of one probe: duplicated,\n * the resource and the tool would drift, and the disagreement would surface as\n * \"the diagnostics lied\" — the one thing this file must never do.\n */\nexport const buildDiagnostics = async (\n client: AppleContactsClient,\n): Promise<Record<string, unknown>> => {\n const status = client.status();\n const located = status.located;\n\n return {\n server: { name: BUILD_INFO.name, version: BUILD_INFO.version },\n // Off means the prompts and the cupertino:// resources are not registered at\n // all. Reported here because this tool still is, so it stays the one place\n // that explains a capability the client cannot see.\n settings: { exposePrompts: client.config.exposePrompts },\n lane: {\n // There is only one, and saying so is more useful than implying a choice.\n reads: \"file lane (read-only SQLite)\",\n writes: \"none — this server registers no mutating tool\",\n appleEvents: \"not used at all, so no Automation grant is needed or requested\",\n },\n stores: {\n directory: located.dirPath,\n directoryListable: located.dirListable,\n found: located.candidates.length,\n opened: status.shards.length,\n sourcesSeen: located.sourceCount,\n totalContacts: status.totalContacts,\n shards: status.shards,\n indexMode: status.indexMode,\n reason: located.reason,\n },\n resolution: {\n phoneSuffixDigits: client.config.phoneSuffixDigits,\n note:\n \"Phone matching uses the last N digits because Contacts stores numbers as typed. \" +\n \"Fewer digits resolves more handles and collides more; the ambiguous count in a \" +\n \"resolve result is how to tell whether a change helped.\",\n },\n caveats: [\n \"Contacts is protected by its own privacy permission, NOT by Full Disk Access, and \" +\n \"unlike Full Disk Access macOS prompts for it. A store that cannot be opened \" +\n \"usually means that prompt was dismissed — re-enable this app under System \" +\n \"Settings > Privacy & Security > Contacts.\",\n \"The address book is spread across several databases: one per account, plus a root \" +\n \"store that is normally almost empty. All readable ones are unioned. If \" +\n \"`opened` is lower than `found`, some accounts are missing from every answer here.\",\n \"A handle that resolves to more than one contact is reported as ambiguous with no \" +\n \"name, never as a guess. Putting the wrong name on a message is worse than \" +\n \"putting none, because it does not look wrong.\",\n \"Contacts held in two accounts are folded together on their link id, matching what \" +\n \"Contacts.app shows as one unified card. A contact with no link id is not folded.\",\n \"This server is read-only by construction. It cannot create, edit or delete a \" +\n \"contact, and enabling writes does not add a tool.\",\n ],\n };\n};\n\n/**\n * What this server can currently do, and why not more.\n *\n * The caveats are the point. Two of them are specific to this surface and both\n * produce a plausible wrong answer rather than an error, which is exactly the\n * kind of thing a caller cannot discover for itself.\n */\nexport const registerDiagnosticsTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_diagnostics\",\n {\n description:\n \"Report which Contacts stores were opened, how many contacts each holds, and what this \" +\n \"server cannot do. Start here when a lookup returns nothing.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => buildDiagnostics(client)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { decodeRef } from \"../client/ref.js\";\nimport { fail, ok, wrapResult } from \"./util.js\";\n\n/**\n * The mutating tools. Registered only when `allowWrites` is on, and never\n * merely refused — an MCP client caches the tool list, so a tool that exists and\n * says no is a tool the model will keep trying.\n *\n * Two verbs, because the dictionary has two. There is no `delete_contacts`:\n * `sdef /System/Applications/Contacts.app` contains no delete command of any\n * kind, and writes go through Apple Events on every surface here because the\n * store is opened `PRAGMA query_only`. See `client/jxa/core.ts`.\n */\n\nconst labelledValue = z.object({\n label: z\n .string()\n .optional()\n .describe('Which kind, e.g. \"mobile\", \"home\", \"work\". Defaults to mobile for phones.'),\n value: z.string().min(1),\n});\n\nconst fields = {\n firstName: z.string().nullable().optional(),\n lastName: z.string().nullable().optional(),\n nickname: z.string().nullable().optional(),\n organization: z.string().nullable().optional(),\n jobTitle: z.string().nullable().optional(),\n department: z.string().nullable().optional(),\n note: z.string().nullable().optional(),\n company: z\n .boolean()\n .optional()\n .describe(\"True for an organisation rather than a person — Contacts shows it differently.\"),\n};\n\n/** The caveat both tools carry, because it is a property of Contacts itself. */\nconst SAVE_CAVEAT =\n \"Contacts keeps edits in an unsaved buffer and its save command commits EVERYTHING pending, so \" +\n \"this also saves any edit someone has half-typed in the Contacts window. That is how the app's \" +\n \"scripting works, not a choice this server makes. The result is re-read after saving, so what \" +\n \"comes back is what Contacts stored rather than what was asked for.\";\n\nexport const registerActionTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_create_contact\",\n {\n description:\n \"Create a new contact in the address book. This is a real card in the user's real \" +\n \"Contacts, and on an iCloud account it syncs to their other devices within seconds. \" +\n \"Give at least one of firstName, lastName or organization. \" +\n SAVE_CAVEAT,\n inputSchema: {\n ...fields,\n phones: z.array(labelledValue).optional(),\n emails: z.array(labelledValue).optional(),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ phones, emails, ...rest }) =>\n wrapResult(async () => {\n // A card with no name at all is not a contact, it is a blank row that\n // has to be found and removed by hand in the UI.\n if (!rest.firstName && !rest.lastName && !rest.organization) {\n return fail(\n \"A contact needs at least one of firstName, lastName or organization — Contacts will \" +\n \"otherwise create a nameless card that is hard to find again.\",\n );\n }\n return ok(\n await client.createContact({\n fields: rest,\n ...(phones ? { phones } : {}),\n ...(emails ? { emails } : {}),\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_contacts_update_contact\",\n {\n description:\n \"Change an existing contact, or add a phone number or email address to one. Omitting a \" +\n \"field leaves it alone; passing null clears it. Phones and emails are ADDED, never \" +\n \"replaced — Contacts models them as separate objects, and there is no way to remove one \" +\n \"through its scripting dictionary. \" +\n SAVE_CAVEAT,\n inputSchema: {\n ref: z\n .string()\n .min(1)\n .describe(\n 'A contact ref from a search or resolve result (looks like \"k1:<account>/<id>\"). ' +\n \"Do not construct one by hand.\",\n ),\n ...fields,\n phones: z.array(labelledValue).optional().describe(\"Added to whatever is already there.\"),\n emails: z.array(labelledValue).optional().describe(\"Added to whatever is already there.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ ref, phones, emails, ...rest }) =>\n wrapResult(async () => {\n const decoded = decodeRef(ref);\n const contact = client.get(decoded.source, decoded.recordPk);\n if (!contact) {\n return fail(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it was ` +\n `removed, since the search ran. Re-run the search to get a current ref.`,\n );\n }\n // The file lane holds the rowid; Apple Events addresses people by their\n // own id. `ZUNIQUEID` is the bridge between the two, and a contact\n // without one cannot be reached from this side at all.\n if (!contact.uniqueId) {\n return fail(\n `The contact \"${contact.displayName}\" has no stable identifier in the address book ` +\n `database, so it cannot be addressed through Contacts' scripting interface. Edit it ` +\n `in Contacts.app instead.`,\n );\n }\n return ok(\n await client.updateContact({\n personId: contact.uniqueId,\n fields: rest,\n ...(phones ? { phones } : {}),\n ...(emails ? { emails } : {}),\n }),\n );\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { encodeRef, decodeRef } from \"../client/ref.js\";\nimport { fail, limitArg, ok, wrap, wrapResult } from \"./util.js\";\n\n/**\n * NOTE ON `async` BELOW: core's `wrap` is typed `() => Promise<T>` because most\n * surfaces reach Apple Events. Contacts reads synchronous SQLite and never\n * leaves the process, so the thunks are marked async here rather than widening a\n * shared signature for every surface to accommodate one.\n */\n\nexport const registerContactTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_search_contacts\",\n {\n description:\n \"Search the address book by name, nickname or organisation. Returns a ref for each \" +\n \"match, plus which account it came from — the same person can legitimately appear twice \" +\n \"if they are in two accounts. Use apple_contacts_get_contact for phone numbers and \" +\n \"email addresses.\",\n inputSchema: {\n query: z.string().min(1).describe(\"Text to look for in names and organisations.\"),\n limit: limitArg,\n },\n annotations: { readOnlyHint: true },\n },\n async ({ query, limit }) =>\n wrap(async () =>\n client.search(query, limit).map((c) => ({\n ref: encodeRef(c.source, c.recordPk),\n name: c.displayName,\n organization: c.organization,\n jobTitle: c.jobTitle,\n account: c.source,\n })),\n ),\n );\n\n server.registerTool(\n \"apple_contacts_list_contacts\",\n {\n description:\n \"List contacts across every account. This is the whole address book, so prefer \" +\n \"apple_contacts_search_contacts when you are looking for someone specific.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true },\n },\n async ({ limit }) =>\n wrap(async () =>\n client.list(limit).map((c) => ({\n ref: encodeRef(c.source, c.recordPk),\n name: c.displayName,\n organization: c.organization,\n account: c.source,\n })),\n ),\n );\n\n server.registerTool(\n \"apple_contacts_get_contact\",\n {\n description:\n \"One contact in full: name, organisation, job title, every phone number and every \" +\n \"email address, each with its label.\",\n inputSchema: {\n ref: z\n .string()\n .min(1)\n .describe(\n 'An opaque contact ref from a list or search result (looks like \"k1:<account>/<id>\"). ' +\n \"Do not construct one by hand.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ ref }) =>\n wrapResult(async () => {\n const decoded = decodeRef(ref);\n const contact = client.get(decoded.source, decoded.recordPk);\n if (!contact) {\n return fail(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it ` +\n `was removed, since the search ran. Re-run the search to get a current ref.`,\n );\n }\n return ok({\n ref,\n name: contact.displayName,\n firstName: contact.firstName,\n lastName: contact.lastName,\n nickname: contact.nickname,\n organization: contact.organization,\n jobTitle: contact.jobTitle,\n account: contact.source,\n isMe: contact.isMe,\n phones: contact.phones,\n emails: contact.emails,\n });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { encodeRef } from \"../client/ref.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * The tool this surface was built for.\n *\n * Everything else here is an ordinary address book server. This one exists\n * because `chat.db` — and a caller ID, and an email header — carries an\n * identifier and no name, and turning one into the other is the only thing on\n * this machine that can.\n */\nexport const registerResolveTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_resolve_handles\",\n {\n description:\n \"Turn phone numbers or email addresses into contact names. Give it the raw identifiers \" +\n \"from somewhere else — Messages handles, a caller ID, an email header — and it returns \" +\n \"one result per handle.\\n\\n\" +\n \"Read `status` on every result rather than assuming a name came back:\\n\" +\n '- \"resolved\" — exactly one contact. `name` is set.\\n' +\n '- \"unknown\" — nobody in the address book has this number. COMMON AND NOT AN ERROR: ' +\n \"measured on a real store, about one in six of even the busiest correspondents does not \" +\n \"resolve. Show the raw handle.\\n\" +\n '- \"ambiguous\" — more than one contact has it, so no name is returned. Do not guess; ' +\n \"`matches` says how many.\\n\" +\n '- \"shortcode\" — a bank, a courier, a 2FA sender. Can never be a contact.\\n\\n' +\n \"Phone matching is by trailing digits, because Contacts stores numbers as typed \" +\n '(\"06 12 34 56 78\") while most systems hand you E.164 (\"+33612345678\"). Exact string ' +\n \"matching resolves almost nothing and is not used.\",\n inputSchema: {\n handles: z\n .array(z.string().min(1))\n .min(1)\n .max(500)\n .describe(\n 'Phone numbers or email addresses, in any format — \"+33612345678\", \"06 12 34 56 78\" ' +\n 'and \"user@example.com\" all work.',\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ handles }) =>\n wrap(async () => {\n const { results, summary } = client.resolve(handles);\n return {\n summary,\n results: results.map((r) => ({\n handle: r.handle,\n kind: r.kind,\n status: r.status,\n name: r.name,\n matches: r.matches,\n ...(r.contact\n ? {\n ref: encodeRef(r.contact.source, r.contact.recordPk),\n organization: r.contact.organization,\n account: r.contact.source,\n }\n : {}),\n })),\n };\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { registerActionTools } from \"./actions.js\";\nimport { registerContactTools } from \"./contacts.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerResolveTools } from \"./resolve.js\";\n\nexport type ToolContext = {\n /**\n * Register the mutating tools too. Off by default — with the flag off they are\n * not merely refused, they are invisible and cannot be called at all, because\n * MCP clients cache the tool list and a tool that exists and says no is one\n * the model will keep trying.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Contacts tools.\n *\n * READS are file-lane and ask for no Automation grant. WRITES are Apple Events,\n * always — the store is opened `PRAGMA query_only` because Contacts owns it and\n * reconciles it against iCloud, so writing to it would corrupt sync state.\n *\n * That split has a cost worth stating plainly: this surface used to need no\n * Automation grant at all, which docs/distribution.md calls the strongest\n * argument for file-first. Turning writes on gives that up — the first write\n * prompts for permission to control Contacts. With `allowWrites` off, nothing\n * here ever sends an Apple Event and the old property still holds.\n *\n * There is no delete. Contacts' scripting dictionary has no delete command of\n * any kind; see `client/jxa/core.ts` for the measurement.\n *\n * The registered set does NOT vary with whether the store is readable. That is a\n * runtime condition which can change while the process lives, and MCP clients\n * cache the tool list, so a tool that appeared and disappeared would leave\n * clients calling names the server no longer has. Tools that need the store\n * report what is missing instead.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleContactsClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client);\n registerContactTools(server, client);\n registerResolveTools(server, client);\n\n if (!ctx.allowWrites) return;\n registerActionTools(server, client);\n};\n","import {\n registerSurfaceResources,\n type Logger,\n type OsascriptRunner,\n} from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleContactsClient } from \"./client/contacts.js\";\nimport type { Config } from \"./config.js\";\nimport { CONTACTS_GUIDE } from \"./guide.js\";\nimport { registerPrompts } from \"./prompts.js\";\nimport { buildDiagnostics } from \"./tools/diagnostics.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\n\nexport type CreateServerOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches real Contacts. */\n osascript?: OsascriptRunner;\n /** Injected by tests so discovery never reaches the developer's real home. */\n home?: string;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleContactsClient;\n};\n\n/**\n * Build the server. Side-effect free: it opens no database and reads no file,\n * so a test can construct it freely and every external dependency arrives\n * through an option.\n */\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n const client = new AppleContactsClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\n ...(opts.osascript ? { osascript: opts.osascript } : {}),\n ...(opts.home ? { home: opts.home } : {}),\n });\n\n registerTools(server, client, { allowWrites: config.allowWrites });\n /*\n * One flag, both primitives — see `exposePrompts` in core's config. A prompt\n * embeds its surface guide, so registering prompts without the resources\n * would leave every expansion naming a `cupertino://…/guide` that this\n * server does not serve.\n */\n if (config.exposePrompts) {\n registerPrompts(server);\n registerSurfaceResources(server, {\n surface: \"contacts\",\n displayName: \"Contacts\",\n guide: CONTACTS_GUIDE,\n diagnostics: () => buildDiagnostics(client),\n // No inventory. The other surfaces have containers you address by name —\n // mailboxes, lists, calendars — and this one does not: you reach a contact\n // by searching for it, never by naming the store it happens to live in.\n // The stores are reported in diagnostics, where they are a permissions\n // fact rather than something to filter on.\n });\n }\n\n return { server, client };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgcrea/mcp-apple-contacts",
3
- "version": "0.0.0-bootstrap",
3
+ "version": "1.3.0",
4
4
  "description": "Model Context Protocol server for the macOS Apple Contacts app",
5
5
  "keywords": [
6
6
  "address-book",
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.30.0",
48
48
  "zod": "^4.4.3",
49
- "@mgcrea/mcp-apple-core": "^1.1.0"
49
+ "@mgcrea/mcp-apple-core": "^1.3.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^26.2.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-BjWSCaMN.js","names":["#col","#entFilter","#contactsFrom","#childRows","#config","#logger","#home","#runner","#located","#indexTried","#index","#require","#lookup","#run","#invalidate","#shapeWrite"],"sources":["../src/build-info.ts","../src/client/errors.ts","../src/client/jxa/core.ts","../src/client/jxa/write.ts","../src/client/locate.ts","../src/client/phone.ts","../src/client/resolve.ts","../src/client/store.ts","../src/client/contacts.ts","../src/client/ref.ts","../src/config.ts","../src/tools/actions.ts","../src/tools/contacts.ts","../src/tools/diagnostics.ts","../src/tools/resolve.ts","../src/tools/index.ts","../src/server.ts"],"sourcesContent":["// Build-time / runtime identity for the running server. `name`/`version` are\n// read from package.json at startup (always accurate); `gitCommit` /\n// `gitCommitDate` are injected by tsdown's `define` substitution at build time\n// and fall back to \"unknown\" when running from source (e.g. vitest).\n\nimport { readPackageIdentity, type BuildInfo } from \"@mgcrea/mcp-apple-core\";\n\n// oxlint-disable no-underscore-dangle -- bundler-injected build-time constants.\ndeclare const __GIT_COMMIT__: string;\ndeclare const __GIT_COMMIT_DATE__: string;\n\nconst pkg = readPackageIdentity(new URL(\"../package.json\", import.meta.url), {\n name: \"@mgcrea/mcp-apple-contacts\",\n version: \"0.0.0\",\n});\n\nexport type { BuildInfo };\n\nexport const BUILD_INFO: BuildInfo = {\n name: pkg.name,\n version: pkg.version,\n gitCommit: typeof __GIT_COMMIT__ === \"string\" ? __GIT_COMMIT__ : \"unknown\",\n gitCommitDate: typeof __GIT_COMMIT_DATE__ === \"string\" ? __GIT_COMMIT_DATE__ : \"unknown\",\n};\n","/**\n * Contacts' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\nexport const CONTACTS_SURFACE: SurfaceContext = {\n appName: \"Contacts\",\n envPrefix: \"APPLE_CONTACTS\",\n};\n\n/**\n * Contacts' Apple Events target — and, like Calendar's `com.apple.iCal`, not the\n * display name. Contacts.app kept the id it shipped with as Address Book;\n * `com.apple.Contacts` does not exist.\n *\n * Used by the write lane only. Reads never send an Apple Event.\n */\nexport const CONTACTS_BUNDLE_ID = \"com.apple.AddressBook\";\n\nexport {\n AppleAutomationError as AppleContactsError,\n AppBusyError as ContactsBusyError,\n AppNotRunningError as ContactsNotRunningError,\n IndexUnavailableError,\n OsascriptTimeoutError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/** A contact ref no longer resolves — deleted, or its account was removed. */\nexport class ContactNotFoundError extends AppleAutomationError {\n override readonly name = \"ContactNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it was ` +\n `removed, since the search ran. Re-run the search to get a current ref.`,\n { ref },\n );\n }\n}\n\n/**\n * The store could not be read, with the reason spelled out.\n *\n * Its own error because Contacts fails differently from every other surface in\n * this repo: it sits behind its own TCC service rather than behind Full Disk\n * Access, and unlike Full Disk Access that permission PROMPTS. So the fix is\n * usually \"answer the dialog\", not \"go to System Settings\" — and telling\n * somebody to grant whole-disk access for an address book would be asking for\n * far more than this server needs.\n */\nexport class ContactsUnavailableError extends AppleAutomationError {\n override readonly name = \"ContactsUnavailableError\";\n\n constructor(reason: string) {\n super(reason, {});\n }\n}\n\n/**\n * A write did not survive the save.\n *\n * Its own error because Contacts fails this way and the other surfaces do not:\n * changes sit in an unsaved buffer until `save()` runs, so a mutation can\n * succeed, read back correctly inside the same script, and still never reach the\n * store. Every write script saves and then re-reads; this is what it raises when\n * the re-read comes back empty.\n */\nexport class ContactWriteNotPersistedError extends AppleAutomationError {\n override readonly name = \"ContactWriteNotPersistedError\";\n\n constructor(message: string) {\n super(\n `${message} Contacts keeps edits in an unsaved buffer, so this usually means the save was ` +\n `refused — check whether Contacts has a modal sheet open, or an account that is read-only.`,\n {},\n );\n }\n}\n\n/**\n * Deleting a contact is not offered, and this says why rather than 404ing.\n *\n * MEASURED, macOS 26.6: the string \"delete\" does not appear anywhere in\n * `sdef /System/Applications/Contacts.app`. The whole command list is make, add,\n * remove and save — `remove` takes a person out of a GROUP, it does not delete\n * them. Writes go through Apple Events on every surface in this repo because the\n * store is `PRAGMA query_only`, so no dictionary verb means no capability.\n */\nexport class ContactDeleteUnsupportedError extends AppleAutomationError {\n override readonly name = \"ContactDeleteUnsupportedError\";\n\n constructor() {\n super(\n `Contacts cannot delete a contact through Apple Events — its scripting dictionary has no ` +\n `delete command at all (make, add, remove and save are the whole list, and \"remove\" only ` +\n `takes someone out of a group). Deleting has to be done in Contacts.app. This server will ` +\n `not write to the address book database directly: it is owned by Contacts and reconciled ` +\n `against iCloud, so writing to it corrupts sync state.`,\n {},\n );\n }\n}\n","/**\n * JXA script fragments for Contacts.\n *\n * Every script here is a static constant. None may contain a template\n * interpolation — `assertStaticScript` rejects any script containing a dollar\n * sign followed by a brace, including template literals written INSIDE the JXA\n * source. Use string concatenation in JXA code.\n *\n * Every script follows the same contract:\n * - it reads its parameters from `JSON.parse(argv[0])`\n * - it returns `JSON.stringify({ok: true, data})` on success\n * - it returns `JSON.stringify({ok: false, error: {code, message}})` on an\n * application-level failure, still exiting 0\n * so a non-zero exit always means infrastructure rather than \"no such contact\".\n *\n * ## There is no read.ts here, and that is still the design\n *\n * Adding writes did not add a read lane. Reads come off the file lane, which is\n * the only place a suffix-keyed index over 970 phone numbers can be built at\n * all — see `../store.ts`. These scripts exist only to make Contacts CHANGE\n * something. `test/jxa.test.ts` asserts `read.ts` does not exist.\n *\n * ## What the dictionary actually offers\n *\n * MEASURED from `sdef /System/Applications/Contacts.app` on macOS 26.6. The\n * whole command list is four verbs:\n *\n * make create a person, or a phone/email element under one\n * add put a person in a group\n * remove take a person out of a group\n * save commit everything\n *\n * That is all of it. The Standard Suite here contains **only `make`** — the\n * string \"delete\" does not appear anywhere in the dictionary. So there is no\n * supported way to delete a contact over Apple Events, which is why this file\n * has no DELETE script and why `delete_contacts` is not a tool. Whether Cocoa\n * Scripting answers an undeclared `delete` event anyway is a separate question\n * and an unmeasured one; guessing at it would risk destroying a real person's\n * card on the strength of an assumption.\n *\n * ## `save` is explicit, global, and the whole reason this is not like Calendar\n *\n * Calendar and Reminders persist a property assignment immediately. Contacts\n * does not: changes sit in an unsaved buffer until `Application(\"Contacts\").save()`\n * runs, and the dictionary says so — the application class carries an `unsaved`\n * property, and `save` is documented as \"Save ALL Contacts changes\".\n *\n * Two consequences, both load-bearing:\n *\n * 1. **A write that forgets `save()` silently does nothing.** The object updates,\n * every read-back inside the same script agrees, and the store never changes.\n * Every script below saves before it verifies.\n * 2. **`save()` is not scoped to our change.** It commits whatever else is\n * pending, including an edit someone has half-typed in the Contacts window.\n * That is a property of the dictionary, not a choice made here, and the tool\n * descriptions say so.\n */\n\n/**\n * Shared prelude.\n *\n * The bundle identifier is `com.apple.AddressBook`, which — like Calendar's\n * `com.apple.iCal` — does not match the display name. Contacts.app kept the id\n * it shipped with as Address Book. `Application(\"Contacts\")` is the correct\n * scripting name; `com.apple.Contacts` does not exist.\n */\nexport const PRELUDE = `\nObjC.import(\"AppKit\");\n\nfunction isContactsRunning() {\n var apps = $.NSRunningApplication.runningApplicationsWithBundleIdentifier(\"com.apple.AddressBook\");\n return apps.count > 0;\n}\n\nfunction ok(data) { return JSON.stringify({ ok: true, data: data }); }\nfunction err(code, message) { return JSON.stringify({ ok: false, error: { code: code, message: String(message) } }); }\n\n/** Read one property defensively: Contacts throws on properties it cannot supply. */\nfunction prop(fn, fallback) {\n try {\n var v = fn();\n return v === undefined ? fallback : v;\n } catch (e) {\n return fallback;\n }\n}\n\n/**\n * Find one person, without assuming the two lanes spell an id the same way.\n *\n * The file lane holds \\`ZABCDRECORD.ZUNIQUEID\\`; Apple Events returns whatever\n * \\`person.id()\\` returns. That those are the same string is EXACTLY the kind of\n * thing this project has been wrong about before — Calendar's \\`calendar.uid()\\`\n * throws for every calendar, and the id bridge that held for its events did not\n * extend to them. It is unmeasured here, so it is not assumed.\n *\n * Two attempts, cheapest first:\n *\n * 1. \\`byId()\\` with the value as given. Contacts offers a real by-id lookup,\n * unlike Calendar, so when the forms do agree this costs one round trip.\n * 2. A bulk \\`people.id()\\` fetch, matched on the UUID substring. This is the\n * guard, and it is affordable precisely here: docs/contacts.md measured the\n * whole id list at 63-73 ms over 421 people, against the 1.8 s that made the\n * same trick unusable for Calendar's events.\n *\n * So a mismatch in id FORM degrades to a fast scan instead of a wrong \"not\n * found\" — which is the failure this would otherwise produce, and the one that\n * looks like the contact was deleted.\n */\nfunction uuidOf(value) {\n var m = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/.exec(String(value || \"\"));\n return m ? m[0].toUpperCase() : null;\n}\n\nfunction findPerson(C, personId) {\n try {\n var direct = C.people.byId(personId);\n direct.id();\n return direct;\n } catch (e) {\n // Fall through to the scan.\n }\n\n var wanted = uuidOf(personId);\n if (!wanted) return null;\n\n try {\n var ids = C.people.id();\n for (var i = 0; i < ids.length; i++) {\n if (uuidOf(ids[i]) === wanted) {\n var found = C.people.byId(ids[i]);\n found.id();\n return found;\n }\n }\n } catch (e2) {\n return null;\n }\n return null;\n}\n\n/** Everything a write returns, so a caller sees what Contacts stored. */\nfunction shapePerson(p) {\n return {\n id: prop(function () { return String(p.id()); }, null),\n name: prop(function () { return p.name(); }, null),\n firstName: prop(function () { return p.firstName(); }, null),\n lastName: prop(function () { return p.lastName(); }, null),\n nickname: prop(function () { return p.nickname(); }, null),\n organization: prop(function () { return p.organization(); }, null),\n jobTitle: prop(function () { return p.jobTitle(); }, null),\n department: prop(function () { return p.department(); }, null),\n note: prop(function () { return p.note(); }, null),\n company: prop(function () { return p.company(); }, false),\n phones: prop(function () {\n var out = [];\n var xs = p.phones();\n for (var i = 0; i < xs.length; i++) {\n out.push({\n label: prop(function () { return xs[i].label(); }, null),\n value: prop(function () { return xs[i].value(); }, null)\n });\n }\n return out;\n }, []),\n emails: prop(function () {\n var out = [];\n var xs = p.emails();\n for (var i = 0; i < xs.length; i++) {\n out.push({\n label: prop(function () { return xs[i].label(); }, null),\n value: prop(function () { return xs[i].value(); }, null)\n });\n }\n return out;\n }, [])\n };\n}\n\n/**\n * Apply the scalar properties a caller supplied.\n *\n * Only keys actually present are touched: a missing key means \"leave it alone\",\n * an explicit null means \"clear it\". Assigning undefined would blank a field the\n * caller never mentioned.\n */\nfunction applyFields(p, f) {\n if (f.firstName !== undefined) p.firstName = f.firstName;\n if (f.lastName !== undefined) p.lastName = f.lastName;\n if (f.nickname !== undefined) p.nickname = f.nickname;\n if (f.organization !== undefined) p.organization = f.organization;\n if (f.jobTitle !== undefined) p.jobTitle = f.jobTitle;\n if (f.department !== undefined) p.department = f.department;\n if (f.note !== undefined) p.note = f.note;\n if (f.company !== undefined) p.company = f.company;\n}\n\n/**\n * Add phone and email elements.\n *\n * These are ELEMENTS, not properties: a phone number is its own object made at\n * the end of the person's phones. There is no way to set them as a bulk array,\n * so each one is a separate \\`make\\`.\n */\nfunction addChildren(C, p, phones, emails) {\n var i;\n if (phones) {\n for (i = 0; i < phones.length; i++) {\n p.phones.push(C.Phone({ label: phones[i].label || \"mobile\", value: phones[i].value }));\n }\n }\n if (emails) {\n for (i = 0; i < emails.length; i++) {\n p.emails.push(C.Email({ label: emails[i].label || \"home\", value: emails[i].value }));\n }\n }\n}\n`;\n","import { PRELUDE } from \"./core.js\";\n\n/**\n * The write scripts. Two verbs, and the absence of a third is deliberate.\n *\n * `create` and `update` are both `make`-and-`save`. There is no `delete`,\n * because the Contacts dictionary has no delete command — see `core.ts` for the\n * measurement. That absence is enforced by `test/jxa.test.ts`, so removing a\n * contact cannot be added here without the decision being taken again.\n *\n * Every script SAVES and then RE-READS, in that order. Contacts keeps changes in\n * an unsaved buffer, so a script that skips the save mutates a live object,\n * reports success from its own in-memory read, and leaves the store untouched.\n * Verifying before saving would find exactly the same false success.\n */\n\nexport const CREATE_CONTACT = `${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0]);\n var C = Application(\"Contacts\");\n\n if (!isContactsRunning() && !p.allowLaunch) {\n return err(\"APP_NOT_RUNNING\", \"Contacts is not running.\");\n }\n\n var person;\n try {\n person = C.Person({\n firstName: p.fields.firstName || \"\",\n lastName: p.fields.lastName || \"\"\n });\n C.people.push(person);\n } catch (e) {\n return err(\"CREATE_FAILED\", e.message || e);\n }\n\n try {\n applyFields(person, p.fields);\n addChildren(C, person, p.phones, p.emails);\n } catch (e) {\n // The person exists but is incomplete. Save anyway so the caller is told\n // about a real half-written card rather than a phantom one, and let the\n // read-back below show exactly what landed.\n try { C.save(); } catch (e2) {}\n return err(\"CREATE_INCOMPLETE\", e.message || e);\n }\n\n try {\n C.save();\n } catch (e) {\n return err(\"SAVE_FAILED\", e.message || e);\n }\n\n // Re-read AFTER the save, so what comes back is what Contacts stored rather\n // than what this script asked for.\n var fresh = findPerson(C, prop(function () { return String(person.id()); }, \"\"));\n if (!fresh) {\n return err(\"CREATE_NOT_PERSISTED\", \"Contacts saved without error but the contact could not be read back.\");\n }\n return ok(shapePerson(fresh));\n}\n`;\n\nexport const UPDATE_CONTACT = `${PRELUDE}\nfunction run(argv) {\n var p = JSON.parse(argv[0]);\n var C = Application(\"Contacts\");\n\n if (!isContactsRunning() && !p.allowLaunch) {\n return err(\"APP_NOT_RUNNING\", \"Contacts is not running.\");\n }\n\n var person = findPerson(C, p.personId);\n if (!person) {\n return err(\"CONTACT_NOT_FOUND\", \"No contact with id \" + p.personId + \".\");\n }\n\n try {\n applyFields(person, p.fields);\n addChildren(C, person, p.phones, p.emails);\n } catch (e) {\n return err(\"UPDATE_FAILED\", e.message || e);\n }\n\n try {\n C.save();\n } catch (e) {\n return err(\"SAVE_FAILED\", e.message || e);\n }\n\n var fresh = findPerson(C, p.personId);\n if (!fresh) {\n return err(\"UPDATE_NOT_PERSISTED\", \"Contacts saved without error but the contact could not be read back.\");\n }\n return ok(shapePerson(fresh));\n}\n`;\n","import { readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport { describeStore, type StoreFacts } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Contacts' stores — plural, which is the whole point of this file.\n *\n * Every other surface in this repo has one store. Contacts has one per account\n * plus a root database, and `docs/contacts.md` measured what that means:\n *\n * AddressBook-v22.abcddb 1 contact\n * Sources/<uuid>/AddressBook-v22.abcddb 420 contacts\n *\n * The obvious path — the one at the top of the directory — is present, readable,\n * correctly shaped, and empty. A server that opens it gets a working database\n * with nobody in it, which fails no check and returns no answer. That is not a\n * hypothetical: `scripts/probe-contacts.mjs` did exactly this and reported a\n * confident 0% resolution rate before anyone noticed.\n *\n * So there is no \"the\" store here. Everything readable is opened and the rows\n * are unioned, and the number of sources is discovered rather than assumed —\n * one on the probed machine, more with Google or Exchange accounts.\n */\n\n/** `~/Library/Application Support/AddressBook`. */\nexport const ADDRESSBOOK_DIR = join(\"Library\", \"Application Support\", \"AddressBook\");\n\n/** The per-account subdirectory. Each child holds one database. */\nexport const SOURCES_DIRNAME = \"Sources\";\n\n/** Constant on every store, root and source alike. */\nexport const STORE_FILENAME = \"AddressBook-v22.abcddb\";\n\nexport type StoreCandidate = StoreFacts & {\n path: string;\n /** `root` for the top-level database, else the source directory name. */\n label: string;\n};\n\nexport type LocateResult = {\n dirPath: string;\n dirListable: boolean;\n /** Every store-shaped file found, root first. */\n candidates: StoreCandidate[];\n /** The subset that can actually be opened. May be empty. */\n readable: StoreCandidate[];\n /** How many `Sources/*` directories were seen, readable or not. */\n sourceCount: number;\n reason: string | null;\n};\n\nexport const defaultDirPath = (home: string = homedir()): string => join(home, ADDRESSBOOK_DIR);\n\n/**\n * The grant hint.\n *\n * Deliberately NOT the Full Disk Access sentence the other surfaces use.\n * Contacts is protected by its own TCC service, and unlike Full Disk Access that\n * one prompts — so the likely fix is a dialog that was dismissed, and the\n * remedy names the Contacts pane rather than asking for the whole disk.\n */\nconst GRANT_HINT =\n \"Contacts is protected by its own privacy permission, not by Full Disk Access. macOS asks for \" +\n \"it the first time something reads the address book; if that dialog was dismissed, re-enable \" +\n \"the app under System Settings > Privacy & Security > Contacts and restart it.\";\n\nconst listDirs = (dir: string): string[] => {\n try {\n return readdirSync(dir, { withFileTypes: true })\n .filter((e) => e.isDirectory() && !e.name.startsWith(\".\"))\n .map((e) => e.name);\n } catch {\n return [];\n }\n};\n\nexport const locateStores = (\n opts: { storePath?: string | undefined; home?: string } = {},\n): LocateResult => {\n const dirPath = defaultDirPath(opts.home);\n\n // An explicit path is a bypass, for tests and forensic copies. No discovery,\n // and no union — the caller said which file it meant.\n if (opts.storePath) {\n const candidate: StoreCandidate = {\n ...describeStore(opts.storePath),\n path: opts.storePath,\n label: \"explicit\",\n };\n return {\n dirPath,\n dirListable: true,\n candidates: [candidate],\n readable: candidate.readable ? [candidate] : [],\n sourceCount: 0,\n reason: candidate.readable\n ? null\n : candidate.exists\n ? `The store at ${opts.storePath} exists but cannot be read. ${GRANT_HINT}`\n : `No file at ${opts.storePath}. APPLE_CONTACTS_STORE points at nothing.`,\n };\n }\n\n const rootPath = join(dirPath, STORE_FILENAME);\n const sourceNames = listDirs(join(dirPath, SOURCES_DIRNAME));\n\n const candidates: StoreCandidate[] = [\n { ...describeStore(rootPath), path: rootPath, label: \"root\" },\n ...sourceNames.map((name) => {\n const path = join(dirPath, SOURCES_DIRNAME, name, STORE_FILENAME);\n return { ...describeStore(path), path, label: name };\n }),\n ].filter((c) => c.exists);\n\n const readable = candidates.filter((c) => c.readable);\n\n // `dirListable` is the signal here, not a store's readability: the root file\n // can be statted without the grant, so \"the file is there\" proves nothing. If\n // the directory cannot be listed then the sources cannot even be enumerated,\n // and the sources are where the contacts are.\n const dirListable = listDirs(dirPath).length > 0 || sourceNames.length > 0;\n\n const reason = readable.length\n ? null\n : candidates.length\n ? `Found ${candidates.length} Contacts store(s) under ${dirPath} but none could be opened. ${GRANT_HINT}`\n : dirListable\n ? `No ${STORE_FILENAME} under ${dirPath}. Has Contacts ever been set up on this account?`\n : `${dirPath} could not be listed, so the per-account stores could not be found. ${GRANT_HINT}`;\n\n return { dirPath, dirListable, candidates, readable, sourceCount: sourceNames.length, reason };\n};\n","/**\n * Phone number matching, which on this surface is the whole product.\n *\n * Contacts stores what the user typed. `docs/contacts.md` measured a 400-row\n * sample of `ZFULLNUMBER`: 222 formatted (`06 12 34 56 78`), 159 already E.164,\n * 15 bare digits. Messages, meanwhile, stores a handle as E.164 and nothing\n * else. So the two never meet as strings, and the measurement says so with an\n * unusually blunt number: **exact string equality resolves 3.7% of message\n * traffic.** A resolver that joins on the stored value is not slightly wrong, it\n * is useless.\n *\n * What works is a SUFFIX. No prefix rule connects `06…` to `+336…` without\n * knowing the user's country, which nothing here has any business guessing, but\n * the two agree from the ninth digit back.\n */\n\n/** Everything that is not a digit, removed. The base of every key below. */\nexport const digitsOf = (value: string): string => value.replaceAll(/\\D/g, \"\");\n\n/**\n * How many trailing digits make a key. Nine, measured rather than picked.\n *\n * | key | recent traffic resolved | ambiguous |\n * | -------- | ----------------------- | --------- |\n * | exact | 8.5% | 1 |\n * | 10 | 96.7% | 5 |\n * | **9** | **97.6%** | **6** |\n * | 7 | 97.6% | 6 |\n *\n * Seven ties nine on every column measured, so nine wins on the tie-break that\n * matters: a shorter key can only ever collide more. Ten is where French\n * national numbers (`0612345678`, ten digits) stop lining up with the same\n * number in E.164 (`+33612345678`, eleven) — which is exactly why 10 does no\n * better than plain digits and 9 does.\n */\nexport const SUFFIX_DIGITS = 9;\n\n/**\n * Below this, a number is a shortcode — a bank, a delivery service, a 2FA\n * sender. 115 of the 958 handles in the measured `chat.db` were these. They can\n * never resolve to a contact, and counting them as failures is how a resolver\n * ends up reporting a far worse rate than it earns.\n */\nconst SHORTCODE_MAX_DIGITS = 6;\n\nexport const isShortcode = (value: string): boolean => {\n const d = digitsOf(value);\n return d.length > 0 && d.length <= SHORTCODE_MAX_DIGITS;\n};\n\n/**\n * The lookup key, or `null` when the value is too short to make one.\n *\n * Returning `null` rather than a short key is deliberate: a three-digit key\n * would match any number ending in those digits, which is the failure mode this\n * whole module exists to avoid.\n */\nexport const suffixKey = (value: string, digits: number = SUFFIX_DIGITS): string | null => {\n const d = digitsOf(value);\n return d.length >= digits ? d.slice(-digits) : null;\n};\n\n/** Email keys are simply case-folded. Measured: 37 of 60 resolve, none ambiguous. */\nexport const emailKey = (value: string): string => value.trim().toLowerCase();\n\nexport type HandleKind = \"phone\" | \"email\" | \"shortcode\";\n\n/**\n * What kind of thing a Messages handle is.\n *\n * Order matters: `@` decides first, because an email address can contain digits\n * and a phone number can never contain an `@`.\n */\nexport const handleKind = (handle: string): HandleKind => {\n if (handle.includes(\"@\")) return \"email\";\n return isShortcode(handle) ? \"shortcode\" : \"phone\";\n};\n","import { emailKey, handleKind, suffixKey, type HandleKind } from \"./phone.js\";\nimport type { HandleLookup, IndexContact } from \"./store.js\";\n\n/**\n * Turn Messages handles into names.\n *\n * This is the function `packages/messages` exists to call, and the reason\n * Contacts was probed at all: `chat.db` records a correspondent as\n * `+15551234567` and nothing else, so a Messages server without this answers\n * \"+15551234567 said …\", which is complete and useless.\n *\n * ## What the measurement says it must do\n *\n * `docs/contacts.md` measured resolution against a real 958-handle store two\n * ways, and the gap between them is the whole design:\n *\n * | denominator | resolved |\n * | -------------------------- | -------- |\n * | every handle ever seen | 27.6% |\n * | messages in the last year | 97.6% |\n * | the 25 busiest correspondents | 84% |\n *\n * The first number is a fact about the address book — 321 handles sent exactly\n * one message, ever — not about this resolver. The last one is the one that\n * shapes the API: **about one in six of the busiest correspondents does not\n * resolve.** So `unknown` is a normal, expected, first-class outcome. It is not\n * an error, it must not throw, and a caller that treats it as a failure will be\n * wrong several times on any real inbox.\n */\n\nexport type ResolutionStatus =\n /** Exactly one contact carries this handle. */\n | \"resolved\"\n /** Nobody does. Normal — see above. */\n | \"unknown\"\n /**\n * More than one distinct contact does.\n *\n * Reported rather than resolved by picking a winner. Six handles collided at\n * nine digits on the probed store, and the failure mode of guessing is putting\n * one person's name on another person's messages — which is worse than no name\n * at all, because it is not visibly wrong.\n */\n | \"ambiguous\"\n /** A shortcode: a bank, a courier, a 2FA sender. Can never be a contact. */\n | \"shortcode\";\n\nexport type ResolvedHandle = {\n handle: string;\n kind: HandleKind;\n status: ResolutionStatus;\n /** The name to show. Null unless `status` is `resolved`. */\n name: string | null;\n contact: IndexContact | null;\n /** How many distinct contacts matched. 0, 1, or more. */\n matches: number;\n};\n\n/**\n * Distinct PEOPLE, not distinct rows.\n *\n * A contact with the same number stored twice (mobile and iPhone, which Contacts\n * does routinely) would otherwise read as ambiguous. Linked records across two\n * accounts are collapsed on `ZLINKID` for the same reason: Contacts shows one\n * unified card for them, and reporting two names for one person would contradict\n * what the user sees in the app.\n */\nconst distinctPeople = (ids: Iterable<string>, lookup: HandleLookup): IndexContact[] => {\n const seen = new Map<string, IndexContact>();\n for (const id of ids) {\n const contact = lookup.contacts.get(id);\n if (!contact) continue;\n // Prefer the link id so cross-account duplicates fold together; fall back to\n // the record's own identity when it carries no link.\n const key = contact.linkId === null ? `pk:${id}` : `link:${contact.linkId}`;\n if (!seen.has(key)) seen.set(key, contact);\n }\n return [...seen.values()];\n};\n\nexport const resolveHandle = (handle: string, lookup: HandleLookup): ResolvedHandle => {\n const kind = handleKind(handle);\n const base = { handle, kind, name: null, contact: null, matches: 0 } as const;\n\n if (kind === \"shortcode\") return { ...base, status: \"shortcode\" };\n\n const ids =\n kind === \"email\"\n ? lookup.byEmail.get(emailKey(handle))\n : (() => {\n const key = suffixKey(handle, lookup.suffixDigits);\n return key === null ? undefined : lookup.byPhone.get(key);\n })();\n\n if (!ids?.size) return { ...base, status: \"unknown\" };\n\n const people = distinctPeople(ids, lookup);\n if (people.length === 1) {\n const contact = people[0]!;\n return { handle, kind, status: \"resolved\", name: contact.displayName, contact, matches: 1 };\n }\n if (people.length === 0) return { ...base, status: \"unknown\" };\n return { ...base, status: \"ambiguous\", matches: people.length };\n};\n\nexport const resolveHandles = (\n handles: readonly string[],\n lookup: HandleLookup,\n): ResolvedHandle[] => handles.map((h) => resolveHandle(h, lookup));\n\n/** Counts by status, for a caller that wants to report coverage honestly. */\nexport const summarise = (results: readonly ResolvedHandle[]): Record<ResolutionStatus, number> => {\n const out: Record<ResolutionStatus, number> = {\n resolved: 0,\n unknown: 0,\n ambiguous: 0,\n shortcode: 0,\n };\n for (const r of results) out[r.status] += 1;\n return out;\n};\n","import type { DatabaseSync } from \"node:sqlite\";\n\nimport {\n columnsOf,\n CORE_DATA_EPOCH_OFFSET,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n SchemaDriftError,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { digitsOf, emailKey, suffixKey, SUFFIX_DIGITS } from \"./phone.js\";\n\n/**\n * Contacts' file lane.\n *\n * Reads only, and there is no Apple Events lane at all — not even a fallback.\n * `docs/contacts.md` measured the dictionary as fast (63 ms for every id, 52 ms\n * for every phone number) and still ruled it out for reads, because the thing\n * this surface exists to do is join 970 stored numbers against a set of handles\n * by suffix, and no amount of round trips gets a keyed index out of `osascript`.\n * The consequence is worth stating: **a read-only surface needs no Automation\n * grant**, so this server never prompts for one.\n *\n * ## Two things measured here that are not obvious from the schema\n *\n * **The store is plural.** See `locate.ts`. Every method on `ContactsIndex` fans\n * out over shards and merges.\n *\n * **`ZABCDRECORD` is not a table of contacts.** It is a Core Data single-table\n * inheritance root, and groups, containers and an info row live in it alongside\n * people — 425 rows for 420 contacts on the probed machine. `Z_ENT` is the only\n * discriminator, and it is resolved through `Z_PRIMARYKEY` BY NAME rather than\n * hardcoded, because Core Data assigns those numbers per model version.\n */\n\n/** Tables the lane cannot work without. */\nconst REQUIRED = [\"ZABCDRECORD\"] as const;\n\n/** The schema this was written against. Named in the drift error, not enforced. */\nconst PROBED_FINGERPRINT = \"4f2871e93f6b\";\nconst PROBED_MACOS = \"26.6\";\n\n/**\n * Entity names that mean \"a person\".\n *\n * `ABCDSubscribedContact` inherits from `ABCDContact` and is included: a contact\n * arriving from a subscribed source is still someone whose name should appear\n * beside their messages. Groups (`ABCDGroup`, `ABCDSmartGroup`) and the\n * bookkeeping entities (`ABCDInfo`, `CNCDContainer`) are not people.\n */\nconst CONTACT_ENTITIES = /^(ABCD)?(Subscribed)?Contact$/i;\n\nexport type StoreCapabilities = {\n fingerprint: string;\n recordColumns: Set<string>;\n phoneColumns: Set<string>;\n emailColumns: Set<string>;\n /** `Z_ENT` values that mean a person. Empty means the filter could not be built. */\n contactEntities: number[];\n hasPhones: boolean;\n hasEmails: boolean;\n hasNotes: boolean;\n epochOffset: number;\n};\n\nexport type IndexContact = {\n recordPk: number;\n /** Stable across runs; the ref is built from it. */\n uniqueId: string | null;\n firstName: string | null;\n lastName: string | null;\n nickname: string | null;\n organization: string | null;\n jobTitle: string | null;\n /** Assembled below — never a raw column, because no single column holds it. */\n displayName: string;\n /** Which store this came from, so a duplicate across accounts is explicable. */\n source: string;\n linkId: number | null;\n isMe: boolean;\n};\n\nexport type ContactPhone = { recordPk: number; value: string; label: string | null };\nexport type ContactEmail = { recordPk: number; value: string; label: string | null };\n\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\nconst text = (v: unknown): string | null => (typeof v === \"string\" && v.length > 0 ? v : null);\n\n/**\n * A name to show, assembled from whatever the record actually carries.\n *\n * Falls through deliberately: plenty of real contacts are an organisation with\n * no person name (a garage, a doctor's office), and plenty are a first name\n * alone. Returning an empty string would put a blank where a sender should be,\n * so the last resort is explicit.\n */\nexport const displayNameOf = (c: {\n firstName: string | null;\n lastName: string | null;\n nickname: string | null;\n organization: string | null;\n}): string => {\n const full = [c.firstName, c.lastName].filter(Boolean).join(\" \").trim();\n return full || c.nickname || c.organization || \"(no name)\";\n};\n\n/**\n * Add one key to one bucket. A null key is skipped, never stored as `\"\"` — a\n * number too short to make a key must not become a key that matches everything.\n */\nconst remember = (map: Map<string, Set<string>>, key: string | null, id: string): void => {\n if (!key) return;\n const bucket = map.get(key);\n if (bucket) bucket.add(id);\n else map.set(key, new Set([id]));\n};\n\n/** One opened database, with what was learned about it. */\nexport type Shard = {\n db: DatabaseSync;\n mode: string;\n caps: StoreCapabilities;\n path: string;\n label: string;\n contacts: number;\n};\n\nexport class ContactsIndex {\n readonly shards: readonly Shard[];\n\n constructor(shards: readonly Shard[]) {\n this.shards = shards;\n }\n\n /** Every shard's fingerprint. More than one distinct value is worth showing. */\n get fingerprints(): string[] {\n return [...new Set(this.shards.map((s) => s.caps.fingerprint))];\n }\n\n get totalContacts(): number {\n return this.shards.reduce((n, s) => n + s.contacts, 0);\n }\n\n /**\n * Project a column, or a typed NULL when this store does not have it.\n *\n * Same guard as the other surfaces: the schema is reverse-engineered and\n * unversioned, so an Apple rename costs one field rather than the lane.\n */\n static #col(present: Set<string>, table: string, name: string, alias: string): string {\n return present.has(name) ? `${table}.\"${name}\" AS ${alias}` : `NULL AS ${alias}`;\n }\n\n static #entFilter(caps: StoreCapabilities, alias: string): string {\n if (!caps.contactEntities.length) return \"\";\n return `WHERE ${alias}.\"Z_ENT\" IN (${caps.contactEntities.join(\", \")})`;\n }\n\n #contactsFrom(shard: Shard, where: string, params: unknown[], limit: number): IndexContact[] {\n const c = shard.caps.recordColumns;\n const col = ContactsIndex.#col;\n const ent = ContactsIndex.#entFilter(shard.caps, \"r\");\n const extra = where ? `${ent ? \"AND\" : \"WHERE\"} ${where}` : \"\";\n const sql = `\n SELECT r.\"Z_PK\" AS recordPk,\n ${col(c, \"r\", \"ZUNIQUEID\", \"uniqueId\")},\n ${col(c, \"r\", \"ZFIRSTNAME\", \"firstName\")},\n ${col(c, \"r\", \"ZLASTNAME\", \"lastName\")},\n ${col(c, \"r\", \"ZNICKNAME\", \"nickname\")},\n ${col(c, \"r\", \"ZORGANIZATION\", \"organization\")},\n ${col(c, \"r\", \"ZJOBTITLE\", \"jobTitle\")},\n ${col(c, \"r\", \"ZLINKID\", \"linkId\")},\n ${col(c, \"r\", \"ZCONTAINERWHERECONTACTISME\", \"isMe\")}\n FROM \"ZABCDRECORD\" r\n ${ent} ${extra}\n ORDER BY r.\"ZLASTNAME\" ASC, r.\"ZFIRSTNAME\" ASC\n LIMIT ${Math.max(1, Math.trunc(limit))}`;\n const rows = shard.db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[];\n return rows.map((r) => {\n const parts = {\n firstName: text(r.firstName),\n lastName: text(r.lastName),\n nickname: text(r.nickname),\n organization: text(r.organization),\n };\n return {\n recordPk: Number(r.recordPk),\n uniqueId: text(r.uniqueId),\n ...parts,\n jobTitle: text(r.jobTitle),\n displayName: displayNameOf(parts),\n source: shard.label,\n linkId: num(r.linkId),\n isMe: num(r.isMe) !== null,\n };\n });\n }\n\n /** Every contact, across every shard. */\n list(limit: number): IndexContact[] {\n return this.shards.flatMap((s) => this.#contactsFrom(s, \"\", [], limit)).slice(0, limit);\n }\n\n /**\n * Name search.\n *\n * `LIKE ? ESCAPE '\\'` with core's `escapeLike`, so a contact called \"100%\n * Design\" can be searched for literally instead of matching everyone.\n */\n search(query: string, limit: number): IndexContact[] {\n const needle = `%${escapeLike(query)}%`;\n const out: IndexContact[] = [];\n for (const shard of this.shards) {\n const c = shard.caps.recordColumns;\n const fields = [\"ZFIRSTNAME\", \"ZLASTNAME\", \"ZNICKNAME\", \"ORGANIZATION\", \"ZORGANIZATION\"]\n .filter((f) => c.has(f))\n .map((f) => `r.\"${f}\" LIKE ? ESCAPE '\\\\'`);\n if (!fields.length) continue;\n out.push(\n ...this.#contactsFrom(\n shard,\n `(${fields.join(\" OR \")})`,\n fields.map(() => needle),\n limit,\n ),\n );\n }\n return out.slice(0, limit);\n }\n\n byPk(shardLabel: string, recordPk: number): IndexContact | null {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return null;\n return this.#contactsFrom(shard, `r.\"Z_PK\" = ?`, [recordPk], 1)[0] ?? null;\n }\n\n #childRows(\n shard: Shard,\n table: string,\n caps: Set<string>,\n valueColumns: readonly string[],\n recordPks?: readonly number[],\n ): { recordPk: number; value: string; label: string | null }[] {\n if (!caps.size) return [];\n const valueCol = valueColumns.find((v) => caps.has(v));\n if (!valueCol || !caps.has(\"ZOWNER\")) return [];\n const scope = recordPks?.length\n ? `AND x.\"ZOWNER\" IN (${recordPks.map(() => \"?\").join(\", \")})`\n : \"\";\n const sql = `\n SELECT x.\"ZOWNER\" AS recordPk,\n x.\"${valueCol}\" AS value,\n ${caps.has(\"ZLABEL\") ? `x.\"ZLABEL\"` : \"NULL\"} AS label\n FROM \"${table}\" x\n WHERE x.\"${valueCol}\" IS NOT NULL AND x.\"${valueCol}\" <> '' ${scope}`;\n const rows = shard.db.prepare(sql).all(...((recordPks ?? []) as never[])) as Record<\n string,\n unknown\n >[];\n return rows.flatMap((r) => {\n const value = text(r.value);\n const recordPk = num(r.recordPk);\n if (!value || recordPk === null) return [];\n return [{ recordPk, value, label: text(r.label) }];\n });\n }\n\n phonesFor(shardLabel: string, recordPks: readonly number[]): ContactPhone[] {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return [];\n return this.#childRows(\n shard,\n \"ZABCDPHONENUMBER\",\n shard.caps.phoneColumns,\n [\"ZFULLNUMBER\"],\n recordPks,\n );\n }\n\n emailsFor(shardLabel: string, recordPks: readonly number[]): ContactEmail[] {\n const shard = this.shards.find((s) => s.label === shardLabel);\n if (!shard) return [];\n return this.#childRows(\n shard,\n \"ZABCDEMAILADDRESS\",\n shard.caps.emailColumns,\n [\"ZADDRESS\", \"ZADDRESSNORMALIZED\"],\n recordPks,\n );\n }\n\n /**\n * The resolver index: every phone suffix and every email, keyed to a contact.\n *\n * Built in one pass over every shard because a handle does not know which\n * account its owner lives in. A key mapping to more than one DISTINCT contact\n * is kept as such — see `resolve.ts`, which reports ambiguity rather than\n * picking. `docs/contacts.md` measured six such collisions at nine digits, and\n * twenty-eight at four.\n */\n buildLookup(suffixDigits: number = SUFFIX_DIGITS): HandleLookup {\n const byPhone = new Map<string, Set<string>>();\n const byEmail = new Map<string, Set<string>>();\n const contacts = new Map<string, IndexContact>();\n\n for (const shard of this.shards) {\n const people = this.#contactsFrom(shard, \"\", [], Number.MAX_SAFE_INTEGER);\n const byPk = new Map(people.map((p) => [p.recordPk, p]));\n for (const p of people) contacts.set(`${shard.label}:${p.recordPk}`, p);\n\n for (const row of this.#childRows(shard, \"ZABCDPHONENUMBER\", shard.caps.phoneColumns, [\n \"ZFULLNUMBER\",\n ])) {\n if (!byPk.has(row.recordPk)) continue;\n remember(byPhone, suffixKey(row.value, suffixDigits), `${shard.label}:${row.recordPk}`);\n }\n for (const row of this.#childRows(shard, \"ZABCDEMAILADDRESS\", shard.caps.emailColumns, [\n \"ZADDRESS\",\n \"ZADDRESSNORMALIZED\",\n ])) {\n if (!byPk.has(row.recordPk)) continue;\n remember(byEmail, emailKey(row.value), `${shard.label}:${row.recordPk}`);\n }\n }\n\n // The key length travels WITH the index. Indexing at nine and querying at\n // seven would match nothing and look exactly like an empty address book.\n return { byPhone, byEmail, contacts, suffixDigits };\n }\n\n close(): void {\n for (const s of this.shards) {\n try {\n s.db.close();\n } catch {\n // Closing a database that already failed is not worth reporting.\n }\n }\n }\n}\n\nexport type HandleLookup = {\n /** Phone suffix → the contacts carrying it. Key length is `suffixDigits`. */\n byPhone: Map<string, Set<string>>;\n /** Case-folded address → the contacts carrying it. */\n byEmail: Map<string, Set<string>>;\n /** `\"<shard>:<pk>\"` → the contact. */\n contacts: Map<string, IndexContact>;\n /** How the phone keys were built. Queries MUST use the same length. */\n suffixDigits: number;\n};\n\nexport const introspect = (db: DatabaseSync): StoreCapabilities => {\n const recordColumns = new Set(columnsOf(db, \"ZABCDRECORD\"));\n\n for (const t of REQUIRED) {\n if (recordColumns.size === 0) {\n throw new SchemaDriftError(\n `This Contacts store has no ${t} table. It was probed on macOS ${PROBED_MACOS} with ` +\n `schema fingerprint ${PROBED_FINGERPRINT} (a PROBE fingerprint — compare it against ` +\n `another probe run, not against the one diagnostics reports); re-run ` +\n `\\`pnpm probe:contacts\\` to see what changed.`,\n );\n }\n }\n\n // By name, never by number. Core Data assigns Z_ENT per model version.\n let contactEntities: number[] = [];\n try {\n const rows = db.prepare(`SELECT Z_ENT AS ent, Z_NAME AS name FROM Z_PRIMARYKEY`).all() as {\n ent: number;\n name: string;\n }[];\n contactEntities = rows.filter((r) => CONTACT_ENTITIES.test(r.name)).map((r) => Number(r.ent));\n } catch {\n // No Z_PRIMARYKEY means no filter. Reported through `contactEntities` being\n // empty rather than guessed at — an unfiltered count is visibly too high,\n // whereas a wrong hardcoded number silently returns the wrong people.\n contactEntities = [];\n }\n\n const phoneColumns = new Set(columnsOf(db, \"ZABCDPHONENUMBER\"));\n const emailColumns = new Set(columnsOf(db, \"ZABCDEMAILADDRESS\"));\n\n return {\n fingerprint: fingerprintSchema(db),\n recordColumns,\n phoneColumns,\n emailColumns,\n contactEntities,\n hasPhones: phoneColumns.size > 0,\n hasEmails: emailColumns.size > 0,\n hasNotes: columnsOf(db, \"ZABCDNOTE\").length > 0,\n // Measured as apple-seconds, but taken from core rather than written out\n // again: being 31 years out is the classic Core Data date bug.\n epochOffset: CORE_DATA_EPOCH_OFFSET,\n };\n};\n\n/** Count the people in one opened shard, with the entity filter applied. */\nexport const countContacts = (db: DatabaseSync, caps: StoreCapabilities): number => {\n const where = caps.contactEntities.length\n ? `WHERE \"Z_ENT\" IN (${caps.contactEntities.join(\", \")})`\n : \"\";\n try {\n const row = db.prepare(`SELECT COUNT(*) AS c FROM \"ZABCDRECORD\" ${where}`).get() as {\n c: number;\n };\n return Number(row.c ?? 0);\n } catch {\n return 0;\n }\n};\n\nexport const openShard = (\n path: string,\n label: string,\n mode: ReadOnlyMode,\n logger?: Logger,\n): Shard | null => {\n try {\n const {\n db,\n mode: used,\n validated,\n } = openReadOnly<StoreCapabilities>(path, mode, {\n label: \"Contacts store\",\n envVar: \"APPLE_CONTACTS_INDEX_MODE\",\n validate: introspect,\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n logger?.debug?.(\n \"opened a Contacts store with immutable=1, which skips the write-ahead log — \" +\n \"very recent edits may be missing until Contacts checkpoints.\",\n ),\n });\n return { db, mode: used, caps: validated, path, label, contacts: countContacts(db, validated) };\n } catch (err) {\n if (err instanceof SchemaDriftError) throw err;\n // One unreadable shard is not a failed lane. The others still answer, and\n // the union is reported with the shard count so a short answer is visible.\n logger?.debug?.(`skipped Contacts store ${label}: ${String(err)}`);\n return null;\n }\n};\n\nexport { digitsOf };\n","import {\n createOsascriptRunner,\n IndexUnavailableError,\n withBusyRetry,\n type Logger,\n type OsascriptRunner,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport {\n CONTACTS_SURFACE,\n ContactNotFoundError,\n ContactWriteNotPersistedError,\n ContactsUnavailableError,\n} from \"./errors.js\";\nimport { CREATE_CONTACT, UPDATE_CONTACT } from \"./jxa/write.js\";\nimport { locateStores, type LocateResult } from \"./locate.js\";\nimport { resolveHandles, summarise, type ResolvedHandle } from \"./resolve.js\";\nimport {\n ContactsIndex,\n openShard,\n type HandleLookup,\n type IndexContact,\n type Shard,\n} from \"./store.js\";\n\n/**\n * Contacts' one lane, orchestrated.\n *\n * There is no lane *choice* here, which is what makes this the smallest client\n * in the repo: no Apple Events fallback, no write path, no cache TTL. What it\n * does own is the two things the store is awkward about — opening several\n * databases instead of one, and building the resolver index lazily, because\n * building it walks every contact and every phone row and most callers only\n * want to list a few names.\n */\n\nexport type CreateClientOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches real Contacts. */\n osascript?: OsascriptRunner;\n /** Injected by tests. */\n home?: string;\n};\n\n/** The scalar fields a write may set. Absent means \"leave alone\". */\nexport type ContactFields = {\n firstName?: string | null;\n lastName?: string | null;\n nickname?: string | null;\n organization?: string | null;\n jobTitle?: string | null;\n department?: string | null;\n note?: string | null;\n company?: boolean;\n};\n\nexport type LabelledValue = { label?: string; value: string };\n\n/** What a write reports: what Contacts stored, re-read after the save. */\nexport type WriteResult = {\n ref: string | null;\n personId: string | null;\n name: string | null;\n organization: string | null;\n phones: { label: string | null; value: string | null }[];\n emails: { label: string | null; value: string | null }[];\n source: \"apple-events\";\n};\n\nexport type LaneStatus = {\n located: LocateResult;\n /** One row per store that opened. */\n shards: { label: string; path: string; mode: string; contacts: number; fingerprint: string }[];\n totalContacts: number;\n indexMode: string;\n};\n\nexport type ContactDetail = IndexContact & {\n phones: { value: string; label: string | null }[];\n emails: { value: string; label: string | null }[];\n};\n\nexport class AppleContactsClient {\n readonly #config: Config;\n readonly #logger: Logger | undefined;\n readonly #home: string | undefined;\n\n readonly #runner: OsascriptRunner;\n\n #located: LocateResult | null = null;\n #index: ContactsIndex | null = null;\n #indexTried = false;\n #lookup: HandleLookup | null = null;\n\n constructor(opts: CreateClientOptions) {\n this.#config = opts.config;\n this.#logger = opts.logger;\n this.#home = opts.home;\n this.#runner =\n opts.osascript ??\n createOsascriptRunner({\n surface: CONTACTS_SURFACE,\n osascriptPath: opts.config.osascriptPath,\n timeoutMs: opts.config.osascriptTimeoutMs,\n ...(opts.logger ? { logger: opts.logger } : {}),\n });\n }\n\n get config(): Config {\n return this.#config;\n }\n\n located(): LocateResult {\n this.#located ??= locateStores({\n storePath: this.#config.storePath,\n ...(this.#home ? { home: this.#home } : {}),\n });\n return this.#located;\n }\n\n /**\n * Open every readable store, once.\n *\n * `indexMode: \"off\"` is honoured as a hard no — it is what the test suite uses\n * so that a machine WITH the grant does not silently read the developer's own\n * address book and pass or fail on data nobody wrote.\n */\n index(): ContactsIndex | null {\n if (this.#indexTried) return this.#index;\n this.#indexTried = true;\n if (this.#config.indexMode === \"off\") return null;\n\n const located = this.located();\n const mode = this.#config.indexMode === \"auto\" ? \"ro\" : this.#config.indexMode;\n const shards: Shard[] = [];\n for (const candidate of located.readable) {\n const shard = openShard(candidate.path, candidate.label, mode, this.#logger);\n if (shard) shards.push(shard);\n }\n this.#index = shards.length ? new ContactsIndex(shards) : null;\n return this.#index;\n }\n\n /**\n * The index, or an error naming what is wrong.\n *\n * Never `[]`. An empty list is the answer to \"you have no contacts\", and this\n * surface has exactly one way to produce that answer wrongly — reading the\n * root store and finding one person in it. Callers get a reason instead.\n */\n #require(): ContactsIndex {\n const index = this.index();\n if (index) return index;\n if (this.#config.indexMode === \"off\") {\n throw new IndexUnavailableError(\n \"The Contacts index is disabled (APPLE_CONTACTS_INDEX_MODE=off). This server has no \" +\n \"other lane, so nothing can be read until it is re-enabled.\",\n );\n }\n throw new ContactsUnavailableError(\n this.located().reason ?? \"No readable Contacts store was found.\",\n );\n }\n\n list(limit?: number): IndexContact[] {\n return this.#require().list(limit ?? this.#config.maxResults);\n }\n\n search(query: string, limit?: number): IndexContact[] {\n return this.#require().search(query, limit ?? this.#config.maxResults);\n }\n\n /** One contact with its phone numbers and email addresses. */\n get(source: string, recordPk: number): ContactDetail | null {\n const index = this.#require();\n const contact = index.byPk(source, recordPk);\n if (!contact) return null;\n return {\n ...contact,\n phones: index.phonesFor(source, [recordPk]).map(({ value, label }) => ({ value, label })),\n emails: index.emailsFor(source, [recordPk]).map(({ value, label }) => ({ value, label })),\n };\n }\n\n /**\n * Built on first use and kept.\n *\n * Walking every contact and every phone row is cheap once (970 rows on the\n * probed store) and pointless per call. There is no TTL: this process does not\n * write to Contacts, and a server that has been running while the user edited\n * their address book is not the case worth optimising for. `diagnostics`\n * reports when it was built.\n */\n lookup(): HandleLookup {\n this.#lookup ??= this.#require().buildLookup(this.#config.phoneSuffixDigits);\n return this.#lookup;\n }\n\n /** The function `packages/messages` is meant to call. */\n resolve(handles: readonly string[]): {\n results: ResolvedHandle[];\n summary: Record<string, number>;\n } {\n const results = resolveHandles(handles, this.lookup());\n return { results, summary: summarise(results) };\n }\n\n // ─── writes ────────────────────────────────────────────────────────────────\n // Apple Events, always. The store is opened `PRAGMA query_only` because\n // Contacts owns it and reconciles it against iCloud, so writing to it would\n // corrupt sync state — the lane policy in docs/distribution.md, not a\n // preference. There is no delete: the dictionary has no such command.\n\n async #run<T>(scriptText: string, params: unknown): Promise<T> {\n try {\n return await withBusyRetry(() => this.#runner.run<T>(scriptText, params));\n } catch (err) {\n const code = (err as { details?: { code?: string } })?.details?.code;\n const message = err instanceof Error ? err.message : String(err);\n if (code === \"CONTACT_NOT_FOUND\") throw new ContactNotFoundError(message);\n if (code === \"CREATE_NOT_PERSISTED\" || code === \"UPDATE_NOT_PERSISTED\") {\n throw new ContactWriteNotPersistedError(message);\n }\n throw err;\n }\n }\n\n /**\n * Invalidate the read lane after a write.\n *\n * The index and the resolver lookup are both built once and kept, so a contact\n * created through Apple Events would otherwise stay invisible to `resolve` for\n * the life of the process — the exact \"wrote it, cannot find it\" confusion the\n * id bridge exists to prevent.\n */\n #invalidate(): void {\n this.#index?.close();\n this.#index = null;\n this.#lookup = null;\n this.#indexTried = false;\n }\n\n #shapeWrite(data: Record<string, unknown>): WriteResult {\n const personId = typeof data.id === \"string\" ? data.id : null;\n const shaped = (key: string) =>\n Array.isArray(data[key])\n ? (data[key] as Record<string, unknown>[]).map((r) => ({\n label: typeof r.label === \"string\" ? r.label : null,\n value: typeof r.value === \"string\" ? r.value : null,\n }))\n : [];\n return {\n // Best effort: the file-lane ref needs a shard and a rowid, and the write\n // lane knows neither. A caller that wants one searches again — which is\n // also the only way to be sure the new row reached the store.\n ref: null,\n personId,\n name: typeof data.name === \"string\" ? data.name : null,\n organization: typeof data.organization === \"string\" ? data.organization : null,\n phones: shaped(\"phones\"),\n emails: shaped(\"emails\"),\n source: \"apple-events\",\n };\n }\n\n async createContact(input: {\n fields: ContactFields;\n phones?: readonly LabelledValue[];\n emails?: readonly LabelledValue[];\n }): Promise<WriteResult> {\n const data = await this.#run<Record<string, unknown>>(CREATE_CONTACT, {\n fields: input.fields,\n phones: input.phones ?? [],\n emails: input.emails ?? [],\n allowLaunch: true,\n });\n this.#invalidate();\n return this.#shapeWrite(data);\n }\n\n async updateContact(input: {\n personId: string;\n fields: ContactFields;\n phones?: readonly LabelledValue[];\n emails?: readonly LabelledValue[];\n }): Promise<WriteResult> {\n const data = await this.#run<Record<string, unknown>>(UPDATE_CONTACT, {\n personId: input.personId,\n fields: input.fields,\n phones: input.phones ?? [],\n emails: input.emails ?? [],\n allowLaunch: true,\n });\n this.#invalidate();\n return this.#shapeWrite(data);\n }\n\n status(): LaneStatus {\n const index = this.index();\n return {\n located: this.located(),\n shards: (index?.shards ?? []).map((s) => ({\n label: s.label,\n path: s.path,\n mode: s.mode,\n contacts: s.contacts,\n fingerprint: s.caps.fingerprint,\n })),\n totalContacts: index?.totalContacts ?? 0,\n indexMode: this.#config.indexMode,\n };\n }\n\n close(): void {\n this.#index?.close();\n this.#index = null;\n this.#lookup = null;\n this.#indexTried = false;\n }\n}\n","import { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\n/**\n * `k1:<account>/<recordPk>` — an opaque handle for one contact.\n *\n * ## Why the account rides along\n *\n * Because the store is plural. A record's `Z_PK` is a rowid, and rowids are only\n * unique WITHIN one database — the root store and each account store number\n * their rows from 1 independently. A bare pk would therefore resolve to a\n * different person depending on which store happened to be read first, which is\n * the kind of bug that produces a plausible wrong answer rather than an error.\n *\n * ## Why not `ZUNIQUEID`\n *\n * It exists and is stable, but it is not what the child tables join on —\n * `ZABCDPHONENUMBER.ZOWNER` points at `Z_PK`. Carrying the pk means a `get`\n * needs no extra lookup, and the account prefix supplies the uniqueness the pk\n * lacks. `uniqueId` is still returned on results for callers that want a\n * durable identity across a re-index.\n *\n * ## Why `k1`\n *\n * `c1:` is Calendar's and `r1:` is Reminders'; `k1` is free and the version\n * prefix keeps a future scheme change additive rather than a silent\n * reinterpretation of refs already sitting in a conversation.\n */\n\nexport const REF_VERSION = \"k1\";\n\nexport type ContactRef = { source: string; recordPk: number };\n\n/**\n * An account label can contain almost anything — it is a directory name, and on\n * the probed machine a UUID — so the pk is anchored as the tail and the label is\n * whatever precedes the last `/`. Splitting on the FIRST separator would break\n * on any label containing one.\n */\nconst REF_PATTERN = /^k1:(.+)\\/(\\d+)$/;\n\nexport class InvalidContactRefError extends AppleAutomationError {\n override readonly name = \"InvalidContactRefError\";\n\n constructor(raw: string) {\n super(\n `\"${raw}\" is not a contact ref. Refs come from apple_contacts_search_contacts or ` +\n `apple_contacts_list_contacts and look like \"k1:<account>/<id>\" — they are opaque and ` +\n `must not be constructed by hand.` +\n (raw.startsWith(\"c1:\") || raw.startsWith(\"r1:\")\n ? ` That one belongs to another surface: \"c1:\" refs are Calendar events and \"r1:\" refs ` +\n `are Reminders.`\n : \"\"),\n { ref: raw },\n );\n }\n}\n\nexport const encodeRef = (source: string, recordPk: number): string =>\n `${REF_VERSION}:${source}/${recordPk}`;\n\nexport const decodeRef = (raw: string): ContactRef => {\n const m = REF_PATTERN.exec(raw.trim());\n if (!m) throw new InvalidContactRefError(raw);\n const recordPk = Number(m[2]);\n if (!Number.isSafeInteger(recordPk) || recordPk <= 0) throw new InvalidContactRefError(raw);\n return { source: m[1]!, recordPk };\n};\n","import {\n BaseConfigSchema,\n parseBool,\n parseConfig,\n parseIntOpt,\n trimmed,\n} from \"@mgcrea/mcp-apple-core\";\nimport { z } from \"zod\";\n\n/**\n * Configuration is environment-only — this server holds no secret at all, its\n * access is the macOS permission the user granted.\n *\n * Note what is ABSENT, and why:\n *\n * - **No `allowWrites` behaviour.** It is inherited from `BaseConfigSchema` and\n * deliberately ignored: this surface registers no mutating tool, so there is\n * nothing for the flag to gate. Editing someone's address book from a tool\n * call was never part of what Contacts was probed for.\n * - **No `osascript` settings in use.** Also inherited, also unused — there is\n * no Apple Events lane here at all, which is what lets this server run without\n * an Automation grant.\n * - **No account allowlist.** Contacts are unioned across accounts precisely so\n * that a handle resolves wherever its owner lives; scoping that by account\n * would reintroduce the bug this surface exists to avoid.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /**\n * Explicit store path. Bypasses discovery — for tests and forensic copies.\n *\n * Naming one file also DISABLES the union, which is the point of having it:\n * a test needs a single known database, not whatever the machine happens to\n * hold.\n */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\n /**\n * Trailing digits that make a phone key.\n *\n * Exposed because the right value is a fact about the user's country, and nine\n * was measured on one machine with mostly French and E.164 numbers. Lower is\n * more forgiving and collides more; the ambiguity count in `diagnostics` is\n * how to tell whether a change helped.\n */\n phoneSuffixDigits: z.number().int().min(6).max(15).default(9),\n}).strict();\n\nexport type Config = z.infer<typeof ConfigSchema>;\n\nexport const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config =>\n parseConfig(ConfigSchema, {\n allowWrites: parseBool(env.APPLE_CONTACTS_ALLOW_WRITES),\n debug: parseBool(env.APPLE_CONTACTS_DEBUG),\n storePath: trimmed(env.APPLE_CONTACTS_STORE),\n indexMode: trimmed(env.APPLE_CONTACTS_INDEX_MODE),\n phoneSuffixDigits: parseIntOpt(env.APPLE_CONTACTS_PHONE_SUFFIX_DIGITS),\n osascriptPath: trimmed(env.APPLE_CONTACTS_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_CONTACTS_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_CONTACTS_MAX_RESULTS),\n });\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { decodeRef } from \"../client/ref.js\";\nimport { fail, ok, wrap } from \"./util.js\";\n\n/**\n * The mutating tools. Registered only when `allowWrites` is on, and never\n * merely refused — an MCP client caches the tool list, so a tool that exists and\n * says no is a tool the model will keep trying.\n *\n * Two verbs, because the dictionary has two. There is no `delete_contacts`:\n * `sdef /System/Applications/Contacts.app` contains no delete command of any\n * kind, and writes go through Apple Events on every surface here because the\n * store is opened `PRAGMA query_only`. See `client/jxa/core.ts`.\n */\n\nconst labelledValue = z.object({\n label: z\n .string()\n .optional()\n .describe('Which kind, e.g. \"mobile\", \"home\", \"work\". Defaults to mobile for phones.'),\n value: z.string().min(1),\n});\n\nconst fields = {\n firstName: z.string().nullable().optional(),\n lastName: z.string().nullable().optional(),\n nickname: z.string().nullable().optional(),\n organization: z.string().nullable().optional(),\n jobTitle: z.string().nullable().optional(),\n department: z.string().nullable().optional(),\n note: z.string().nullable().optional(),\n company: z\n .boolean()\n .optional()\n .describe(\"True for an organisation rather than a person — Contacts shows it differently.\"),\n};\n\n/** The caveat both tools carry, because it is a property of Contacts itself. */\nconst SAVE_CAVEAT =\n \"Contacts keeps edits in an unsaved buffer and its save command commits EVERYTHING pending, so \" +\n \"this also saves any edit someone has half-typed in the Contacts window. That is how the app's \" +\n \"scripting works, not a choice this server makes. The result is re-read after saving, so what \" +\n \"comes back is what Contacts stored rather than what was asked for.\";\n\nexport const registerActionTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_create_contact\",\n {\n description:\n \"Create a new contact in the address book. This is a real card in the user's real \" +\n \"Contacts, and on an iCloud account it syncs to their other devices within seconds. \" +\n \"Give at least one of firstName, lastName or organization. \" +\n SAVE_CAVEAT,\n inputSchema: {\n ...fields,\n phones: z.array(labelledValue).optional(),\n emails: z.array(labelledValue).optional(),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ phones, emails, ...rest }) =>\n wrap(async () => {\n // A card with no name at all is not a contact, it is a blank row that\n // has to be found and removed by hand in the UI.\n if (!rest.firstName && !rest.lastName && !rest.organization) {\n return fail(\n \"A contact needs at least one of firstName, lastName or organization — Contacts will \" +\n \"otherwise create a nameless card that is hard to find again.\",\n );\n }\n return ok(\n await client.createContact({\n fields: rest,\n ...(phones ? { phones } : {}),\n ...(emails ? { emails } : {}),\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_contacts_update_contact\",\n {\n description:\n \"Change an existing contact, or add a phone number or email address to one. Omitting a \" +\n \"field leaves it alone; passing null clears it. Phones and emails are ADDED, never \" +\n \"replaced — Contacts models them as separate objects, and there is no way to remove one \" +\n \"through its scripting dictionary. \" +\n SAVE_CAVEAT,\n inputSchema: {\n ref: z\n .string()\n .min(1)\n .describe(\n 'A contact ref from a search or resolve result (looks like \"k1:<account>/<id>\"). ' +\n \"Do not construct one by hand.\",\n ),\n ...fields,\n phones: z.array(labelledValue).optional().describe(\"Added to whatever is already there.\"),\n emails: z.array(labelledValue).optional().describe(\"Added to whatever is already there.\"),\n },\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },\n },\n async ({ ref, phones, emails, ...rest }) =>\n wrap(async () => {\n const decoded = decodeRef(ref);\n const contact = client.get(decoded.source, decoded.recordPk);\n if (!contact) {\n return fail(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it was ` +\n `removed, since the search ran. Re-run the search to get a current ref.`,\n );\n }\n // The file lane holds the rowid; Apple Events addresses people by their\n // own id. `ZUNIQUEID` is the bridge between the two, and a contact\n // without one cannot be reached from this side at all.\n if (!contact.uniqueId) {\n return fail(\n `The contact \"${contact.displayName}\" has no stable identifier in the address book ` +\n `database, so it cannot be addressed through Contacts' scripting interface. Edit it ` +\n `in Contacts.app instead.`,\n );\n }\n return ok(\n await client.updateContact({\n personId: contact.uniqueId,\n fields: rest,\n ...(phones ? { phones } : {}),\n ...(emails ? { emails } : {}),\n }),\n );\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { encodeRef, decodeRef } from \"../client/ref.js\";\nimport { fail, limitArg, ok, wrap } from \"./util.js\";\n\n/**\n * NOTE ON `async` BELOW: core's `wrap` is typed `() => Promise<T>` because most\n * surfaces reach Apple Events. Contacts reads synchronous SQLite and never\n * leaves the process, so the thunks are marked async here rather than widening a\n * shared signature for every surface to accommodate one.\n */\n\nexport const registerContactTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_search_contacts\",\n {\n description:\n \"Search the address book by name, nickname or organisation. Returns a ref for each \" +\n \"match, plus which account it came from — the same person can legitimately appear twice \" +\n \"if they are in two accounts. Use apple_contacts_get_contact for phone numbers and \" +\n \"email addresses.\",\n inputSchema: {\n query: z.string().min(1).describe(\"Text to look for in names and organisations.\"),\n limit: limitArg,\n },\n annotations: { readOnlyHint: true },\n },\n async ({ query, limit }) =>\n wrap(async () =>\n client.search(query, limit).map((c) => ({\n ref: encodeRef(c.source, c.recordPk),\n name: c.displayName,\n organization: c.organization,\n jobTitle: c.jobTitle,\n account: c.source,\n })),\n ),\n );\n\n server.registerTool(\n \"apple_contacts_list_contacts\",\n {\n description:\n \"List contacts across every account. This is the whole address book, so prefer \" +\n \"apple_contacts_search_contacts when you are looking for someone specific.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true },\n },\n async ({ limit }) =>\n wrap(async () =>\n client.list(limit).map((c) => ({\n ref: encodeRef(c.source, c.recordPk),\n name: c.displayName,\n organization: c.organization,\n account: c.source,\n })),\n ),\n );\n\n server.registerTool(\n \"apple_contacts_get_contact\",\n {\n description:\n \"One contact in full: name, organisation, job title, every phone number and every \" +\n \"email address, each with its label.\",\n inputSchema: {\n ref: z\n .string()\n .min(1)\n .describe(\n 'An opaque contact ref from a list or search result (looks like \"k1:<account>/<id>\"). ' +\n \"Do not construct one by hand.\",\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ ref }) =>\n wrap(async () => {\n const decoded = decodeRef(ref);\n const contact = client.get(decoded.source, decoded.recordPk);\n if (!contact) {\n return fail(\n `No contact for ref \"${ref}\". It was probably deleted, or the account holding it ` +\n `was removed, since the search ran. Re-run the search to get a current ref.`,\n );\n }\n return ok({\n ref,\n name: contact.displayName,\n firstName: contact.firstName,\n lastName: contact.lastName,\n nickname: contact.nickname,\n organization: contact.organization,\n jobTitle: contact.jobTitle,\n account: contact.source,\n isMe: contact.isMe,\n phones: contact.phones,\n emails: contact.emails,\n });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"../build-info.js\";\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * What this server can currently do, and why not more.\n *\n * The caveats are the point. Two of them are specific to this surface and both\n * produce a plausible wrong answer rather than an error, which is exactly the\n * kind of thing a caller cannot discover for itself.\n */\nexport const registerDiagnosticsTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_diagnostics\",\n {\n description:\n \"Report which Contacts stores were opened, how many contacts each holds, and what this \" +\n \"server cannot do. Start here when a lookup returns nothing.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () =>\n wrap(async () => {\n const status = client.status();\n const located = status.located;\n\n return {\n server: { name: BUILD_INFO.name, version: BUILD_INFO.version },\n lane: {\n // There is only one, and saying so is more useful than implying a choice.\n reads: \"file lane (read-only SQLite)\",\n writes: \"none — this server registers no mutating tool\",\n appleEvents: \"not used at all, so no Automation grant is needed or requested\",\n },\n stores: {\n directory: located.dirPath,\n directoryListable: located.dirListable,\n found: located.candidates.length,\n opened: status.shards.length,\n sourcesSeen: located.sourceCount,\n totalContacts: status.totalContacts,\n shards: status.shards,\n indexMode: status.indexMode,\n reason: located.reason,\n },\n resolution: {\n phoneSuffixDigits: client.config.phoneSuffixDigits,\n note:\n \"Phone matching uses the last N digits because Contacts stores numbers as typed. \" +\n \"Fewer digits resolves more handles and collides more; the ambiguous count in a \" +\n \"resolve result is how to tell whether a change helped.\",\n },\n caveats: [\n \"Contacts is protected by its own privacy permission, NOT by Full Disk Access, and \" +\n \"unlike Full Disk Access macOS prompts for it. A store that cannot be opened \" +\n \"usually means that prompt was dismissed — re-enable this app under System \" +\n \"Settings > Privacy & Security > Contacts.\",\n \"The address book is spread across several databases: one per account, plus a root \" +\n \"store that is normally almost empty. All readable ones are unioned. If \" +\n \"`opened` is lower than `found`, some accounts are missing from every answer here.\",\n \"A handle that resolves to more than one contact is reported as ambiguous with no \" +\n \"name, never as a guess. Putting the wrong name on a message is worse than \" +\n \"putting none, because it does not look wrong.\",\n \"Contacts held in two accounts are folded together on their link id, matching what \" +\n \"Contacts.app shows as one unified card. A contact with no link id is not folded.\",\n \"This server is read-only by construction. It cannot create, edit or delete a \" +\n \"contact, and enabling writes does not add a tool.\",\n ],\n };\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { encodeRef } from \"../client/ref.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * The tool this surface was built for.\n *\n * Everything else here is an ordinary address book server. This one exists\n * because `chat.db` — and a caller ID, and an email header — carries an\n * identifier and no name, and turning one into the other is the only thing on\n * this machine that can.\n */\nexport const registerResolveTools = (server: McpServer, client: AppleContactsClient): void => {\n server.registerTool(\n \"apple_contacts_resolve_handles\",\n {\n description:\n \"Turn phone numbers or email addresses into contact names. Give it the raw identifiers \" +\n \"from somewhere else — Messages handles, a caller ID, an email header — and it returns \" +\n \"one result per handle.\\n\\n\" +\n \"Read `status` on every result rather than assuming a name came back:\\n\" +\n '- \"resolved\" — exactly one contact. `name` is set.\\n' +\n '- \"unknown\" — nobody in the address book has this number. COMMON AND NOT AN ERROR: ' +\n \"measured on a real store, about one in six of even the busiest correspondents does not \" +\n \"resolve. Show the raw handle.\\n\" +\n '- \"ambiguous\" — more than one contact has it, so no name is returned. Do not guess; ' +\n \"`matches` says how many.\\n\" +\n '- \"shortcode\" — a bank, a courier, a 2FA sender. Can never be a contact.\\n\\n' +\n \"Phone matching is by trailing digits, because Contacts stores numbers as typed \" +\n '(\"06 12 34 56 78\") while most systems hand you E.164 (\"+33612345678\"). Exact string ' +\n \"matching resolves almost nothing and is not used.\",\n inputSchema: {\n handles: z\n .array(z.string().min(1))\n .min(1)\n .max(500)\n .describe(\n 'Phone numbers or email addresses, in any format — \"+33612345678\", \"06 12 34 56 78\" ' +\n 'and \"user@example.com\" all work.',\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ handles }) =>\n wrap(async () => {\n const { results, summary } = client.resolve(handles);\n return {\n summary,\n results: results.map((r) => ({\n handle: r.handle,\n kind: r.kind,\n status: r.status,\n name: r.name,\n matches: r.matches,\n ...(r.contact\n ? {\n ref: encodeRef(r.contact.source, r.contact.recordPk),\n organization: r.contact.organization,\n account: r.contact.source,\n }\n : {}),\n })),\n };\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleContactsClient } from \"../client/contacts.js\";\nimport { registerActionTools } from \"./actions.js\";\nimport { registerContactTools } from \"./contacts.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerResolveTools } from \"./resolve.js\";\n\nexport type ToolContext = {\n /**\n * Register the mutating tools too. Off by default — with the flag off they are\n * not merely refused, they are invisible and cannot be called at all, because\n * MCP clients cache the tool list and a tool that exists and says no is one\n * the model will keep trying.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Contacts tools.\n *\n * READS are file-lane and ask for no Automation grant. WRITES are Apple Events,\n * always — the store is opened `PRAGMA query_only` because Contacts owns it and\n * reconciles it against iCloud, so writing to it would corrupt sync state.\n *\n * That split has a cost worth stating plainly: this surface used to need no\n * Automation grant at all, which docs/distribution.md calls the strongest\n * argument for file-first. Turning writes on gives that up — the first write\n * prompts for permission to control Contacts. With `allowWrites` off, nothing\n * here ever sends an Apple Event and the old property still holds.\n *\n * There is no delete. Contacts' scripting dictionary has no delete command of\n * any kind; see `client/jxa/core.ts` for the measurement.\n *\n * The registered set does NOT vary with whether the store is readable. That is a\n * runtime condition which can change while the process lives, and MCP clients\n * cache the tool list, so a tool that appeared and disappeared would leave\n * clients calling names the server no longer has. Tools that need the store\n * report what is missing instead.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleContactsClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client);\n registerContactTools(server, client);\n registerResolveTools(server, client);\n\n if (!ctx.allowWrites) return;\n registerActionTools(server, client);\n};\n","import type { Logger, OsascriptRunner } from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleContactsClient } from \"./client/contacts.js\";\nimport type { Config } from \"./config.js\";\nimport { registerTools } from \"./tools/index.js\";\n\nexport const SERVER_NAME = BUILD_INFO.name;\nexport const SERVER_VERSION = BUILD_INFO.version;\n\nexport type CreateServerOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so nothing spawns a process or touches real Contacts. */\n osascript?: OsascriptRunner;\n /** Injected by tests so discovery never reaches the developer's real home. */\n home?: string;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleContactsClient;\n};\n\n/**\n * Build the server. Side-effect free: it opens no database and reads no file,\n * so a test can construct it freely and every external dependency arrives\n * through an option.\n */\nexport const createServer = (opts: CreateServerOptions): CreatedServer => {\n const { config } = opts;\n const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });\n\n const client = new AppleContactsClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\n ...(opts.osascript ? { osascript: opts.osascript } : {}),\n ...(opts.home ? { home: opts.home } : {}),\n });\n\n registerTools(server, client, { allowWrites: config.allowWrites });\n\n return { server, client };\n};\n"],"mappings":"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"}