@mgcrea/mcp-apple-maps 1.4.0 → 1.6.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 { C as MAPS_SURFACE, N as BUILD_INFO, a as loadConfig, r as createServer } from "./server-
|
|
2
|
+
import { C as MAPS_SURFACE, N as BUILD_INFO, a as loadConfig, r as createServer } from "./server-Bum_vDw3.js";
|
|
3
3
|
import { runStdioServer } from "@mgcrea/mcp-apple-core";
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
const LOG_PREFIX = "apple-maps-mcp";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as fromStoreTime, C as MAPS_SURFACE, D as UndatableStoreError, E as SchemaDriftError, M as resolveEpoch, N as BUILD_INFO, O as APPLE_SECONDS, S as MAPS_BUNDLE_ID, T as PlaceNotFoundError, _ as defaultDirectory, a as loadConfig, b as AppleMapsError, c as introspect, d as InvalidMapsRefError, f as PLACE_REF_VERSION, g as encodePlaceRef, h as encodeCollectionRef, i as registerTools, j as renderInstant, k as CORE_DATA_EPOCH_OFFSET, l as openStore, m as decodePlaceRef, n as SERVER_VERSION, o as AppleMapsClient, p as decodeCollectionRef, r as createServer, s as MapsStore, t as SERVER_NAME, u as COLLECTION_REF_VERSION, v as defaultStorePath, w as MapsStoreUnavailableError, x as IndexUnavailableError, y as locateStore } from "./server-
|
|
1
|
+
import { A as fromStoreTime, C as MAPS_SURFACE, D as UndatableStoreError, E as SchemaDriftError, M as resolveEpoch, N as BUILD_INFO, O as APPLE_SECONDS, S as MAPS_BUNDLE_ID, T as PlaceNotFoundError, _ as defaultDirectory, a as loadConfig, b as AppleMapsError, c as introspect, d as InvalidMapsRefError, f as PLACE_REF_VERSION, g as encodePlaceRef, h as encodeCollectionRef, i as registerTools, j as renderInstant, k as CORE_DATA_EPOCH_OFFSET, l as openStore, m as decodePlaceRef, n as SERVER_VERSION, o as AppleMapsClient, p as decodeCollectionRef, r as createServer, s as MapsStore, t as SERVER_NAME, u as COLLECTION_REF_VERSION, v as defaultStorePath, w as MapsStoreUnavailableError, x as IndexUnavailableError, y as locateStore } from "./server-Bum_vDw3.js";
|
|
2
2
|
export { APPLE_SECONDS, AppleMapsClient, AppleMapsError, BUILD_INFO, COLLECTION_REF_VERSION, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, InvalidMapsRefError, MAPS_BUNDLE_ID, MAPS_SURFACE, MapsStore, MapsStoreUnavailableError, PLACE_REF_VERSION, PlaceNotFoundError, SERVER_NAME, SERVER_VERSION, SchemaDriftError, UndatableStoreError, createServer, decodeCollectionRef, decodePlaceRef, defaultDirectory, defaultStorePath, encodeCollectionRef, encodePlaceRef, fromStoreTime, introspect, loadConfig, locateStore, openStore, registerTools, renderInstant, resolveEpoch };
|
|
@@ -15,8 +15,8 @@ const pkg = readPackageIdentity(new URL("../package.json", import.meta.url), {
|
|
|
15
15
|
const BUILD_INFO = {
|
|
16
16
|
name: pkg.name,
|
|
17
17
|
version: pkg.version,
|
|
18
|
-
gitCommit: "
|
|
19
|
-
gitCommitDate: "2026-
|
|
18
|
+
gitCommit: "90f269d",
|
|
19
|
+
gitCommitDate: "2026-09-01T00:21:33+02:00"
|
|
20
20
|
};
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/client/dates.ts
|
|
@@ -2077,4 +2077,4 @@ const createServer = (opts) => {
|
|
|
2077
2077
|
//#endregion
|
|
2078
2078
|
export { fromStoreTime as A, MAPS_SURFACE as C, UndatableStoreError as D, SchemaDriftError$1 as E, resolveEpoch as M, BUILD_INFO as N, APPLE_SECONDS as O, MAPS_BUNDLE_ID as S, PlaceNotFoundError as T, defaultDirectory as _, loadConfig as a, AppleMapsError as b, introspect as c, InvalidMapsRefError as d, PLACE_REF_VERSION as f, encodePlaceRef as g, encodeCollectionRef as h, registerTools as i, renderInstant as j, CORE_DATA_EPOCH_OFFSET as k, openStore as l, decodePlaceRef as m, SERVER_VERSION as n, AppleMapsClient as o, decodeCollectionRef as p, createServer as r, MapsStore as s, SERVER_NAME as t, COLLECTION_REF_VERSION as u, defaultStorePath as v, MapsStoreUnavailableError as w, IndexUnavailableError as x, locateStore as y };
|
|
2079
2079
|
|
|
2080
|
-
//# sourceMappingURL=server-
|
|
2080
|
+
//# sourceMappingURL=server-Bum_vDw3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-DgIy0w0S.js","names":["#col","#colText","#unfiledClause","#entityFor","#placeSelect","#toPlace","#keyClause","#path","#openUrl","#seedTimeoutMs","#existingFavorite","#donorNear","#maxHistory","#seededDonor","#open","#entity","#nextPk","#config","#logger","#home","#located","#store","#storeError","#render","#renderCollection"],"sources":["../src/build-info.ts","../src/client/dates.ts","../src/client/errors.ts","../src/client/locate.ts","../src/client/ref.ts","../src/client/store.ts","../src/client/write.ts","../src/client/maps.ts","../src/config.ts","../src/guide.ts","../src/prompts.ts","../src/tools/util.ts","../src/tools/diagnostics.ts","../src/tools/places.ts","../src/tools/writes.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-maps\",\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 * Date handling for the Maps tools.\n *\n * Deliberately much smaller than `packages/safari/src/client/dates.ts`. That\n * surface needed a whole input grammar because history is a range query; Maps'\n * entities are lists a person curates, and v1 ships no tool taking a date\n * argument. So this module solves only the OUTPUT half — turning a stored\n * number into an instant. Inventing a parser nothing calls would be a fourth\n * copy of a grammar `packages/safari` has already noted should be hoisted into\n * core rather than duplicated again.\n *\n * ## The epoch is DETECTED, never assumed\n *\n * `docs/maps.md` records the measurement: `ZCREATETIME` resolves to\n * **apple-seconds** (Core Data, 2001 anchor). The same value read as unix\n * seconds lands in **1995** — well-formed, plausible at a glance, and wrong by\n * 31 years. `pnpm probe:maps` prints both readings side by side precisely\n * because the wrong one does not announce itself.\n *\n * So the offset is resolved from the store's own newest timestamp at open time,\n * and when it cannot be resolved every date renders `null` rather than being\n * guessed. An absent timestamp is a visible gap somebody can report; a\n * confidently wrong one is not. This is the discipline `packages/safari`\n * arrived at after a probe misread its own column — adopted here rather than\n * re-derived.\n */\n\nimport { CORE_DATA_EPOCH_OFFSET, detectEpoch } from \"@mgcrea/mcp-apple-core\";\n\nexport { CORE_DATA_EPOCH_OFFSET };\n\n/**\n * How this store's timestamps map onto real time.\n *\n * `confident` is the field that matters. `detectEpoch` always returns an offset\n * — it falls back to unix when nothing fits — so the offset alone cannot\n * distinguish \"measured as unix\" from \"gave up and assumed unix\". Rendering the\n * second as though it were the first is the failure this module exists to\n * prevent, so the two are kept apart.\n */\nexport type Epoch = {\n offset: number;\n reason: string;\n confident: boolean;\n};\n\n/** What `detectEpoch` says when it has matched nothing. Same shape as Safari's. */\nconst GAVE_UP = /^(no dated rows|neither epoch)/;\n\nexport const resolveEpoch = (maxTimestamp: number | null, now: number = Date.now()): Epoch => {\n const { offset, reason } = detectEpoch(maxTimestamp, now);\n return { offset, reason, confident: !GAVE_UP.test(reason) };\n};\n\n/** The expectation docs/maps.md carries, used only where no store is open. */\nexport const APPLE_SECONDS: Epoch = {\n offset: CORE_DATA_EPOCH_OFFSET,\n reason: \"assumed apple-seconds; no store was opened to measure against\",\n confident: false,\n};\n\n/** A stored timestamp to a JS Date, or null when it cannot be placed. */\nexport const fromStoreTime = (value: number | null, epoch: Epoch): Date | null => {\n if (value === null || !Number.isFinite(value) || value === 0) return null;\n if (!epoch.confident) return null;\n const date = new Date((value + epoch.offset) * 1000);\n return Number.isNaN(date.getTime()) ? null : date;\n};\n\n/** ISO-8601, or null. What every date field on a result carries. */\nexport const renderInstant = (value: number | null, epoch: Epoch): string | null =>\n fromStoreTime(value, epoch)?.toISOString() ?? null;\n","/**\n * Maps' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against, plus the\n * failures no other surface has.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\nexport const MAPS_SURFACE: SurfaceContext = {\n appName: \"Maps\",\n envPrefix: \"APPLE_MAPS\",\n};\n\n/**\n * Maps is one of the few Apple apps whose bundle id matches its display name —\n * unlike `com.apple.iCal`, `com.apple.AddressBook` and `com.apple.MobileSMS`.\n * Stated rather than assumed, because this repo has been caught by the opposite\n * three times.\n *\n * It is recorded here and used by almost nothing. Maps ships **no scripting\n * dictionary** — `/System/Applications/Maps.app` contains no `.sdef`, checked\n * directly rather than inferred from `NSAppleScriptEnabled` — so there is no\n * Apple Events lane to address, and this server never sends one.\n */\nexport const MAPS_BUNDLE_ID = \"com.apple.Maps\";\n\nexport {\n AppleAutomationError as AppleMapsError,\n IndexUnavailableError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/**\n * The store could not be read.\n *\n * This surface has **one lane and no fallback**, which puts it with Messages\n * rather than with Safari: without Full Disk Access there is no degraded Maps\n * server, there is no Maps server. So every read throws this rather than\n * returning an empty list — an empty `favorites` reads exactly like a person\n * who has saved no places, and that is the failure this error exists to\n * prevent.\n *\n * The hint names the trap that actually catches people: the store lives at a\n * path with **no file extension**, inside the one directory in Maps' container\n * that Full Disk Access gates. A sweep for `*.db` finds nothing and concludes\n * the data is not on disk. It is.\n */\nexport class MapsStoreUnavailableError extends AppleAutomationError {\n override readonly name = \"MapsStoreUnavailableError\";\n\n constructor(reason: string) {\n super(\n `${reason} Maps has no second lane — it ships no scripting dictionary, so there is no ` +\n `Apple Events fallback and nothing at all can be read without the grant.`,\n {},\n );\n }\n}\n\n/** A ref no longer resolves — the place was removed, or the store was re-synced. */\nexport class PlaceNotFoundError extends AppleAutomationError {\n override readonly name = \"PlaceNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No place for ref \"${ref}\". It may have been removed in Maps, or iCloud may have ` +\n `re-synced the store and renumbered it. Re-run the listing to get a current ref.`,\n { ref },\n );\n }\n}\n\n/**\n * The epoch could not be identified from the data.\n *\n * The same discipline `packages/safari` applies, for the same measured reason:\n * a wrong epoch produces dates that are well-formed and wrong by 31 years.\n * `docs/maps.md` records that this store's `ZCREATETIME` read as apple-seconds\n * and that a unix-seconds reading of the same value lands in 1995 while still\n * looking entirely plausible. So the offset is detected from the store at open\n * time and dates are WITHHELD when it cannot be.\n */\nexport class UndatableStoreError extends AppleAutomationError {\n override readonly name = \"UndatableStoreError\";\n\n constructor(reason: string) {\n super(\n `Maps' timestamps could not be placed on a known epoch (${reason}). Dates are withheld ` +\n `rather than guessed — a timestamp wrong by decades reads exactly like a correct one. ` +\n `Everything that does not depend on a date still works.`,\n {},\n );\n }\n}\n","import { readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport {\n describeStore,\n inspectFile,\n type FileFacts,\n type StoreFacts,\n} from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Maps' store.\n *\n * ## The path, and the three ways it was missed\n *\n * `~/Library/Containers/com.apple.Maps/Data/Maps/MapsSync_0.0.1` — 3.3 MB of\n * Core Data on the probed machine. `docs/maps.md` records that this surface was\n * declared \"no file lane\" three separate times before the file was found, and\n * each failure is a rule this locator is shaped by:\n *\n * 1. **The file has no extension.** A sweep for `*.db` / `*.sqlite*` finds\n * nothing here. Nothing in this file matches on a suffix.\n * 2. **`Data/Maps/` is gated.** It was the ONE unreadable directory in the\n * container, so a listing taken without Full Disk Access omits it, and the\n * omission reads as absence. `inspectFile` splits exists from readable\n * precisely so those two never collapse into one answer.\n * 3. **`group.com.apple.Maps` is a decoy.** It exists, it is EPERM without the\n * grant, and it holds three files that are not the store. A probe that\n * could not descend into it reported it as empty.\n *\n * The rule underneath all three is the one `docs/surfaces.md` states: \"'Absent'\n * and 'EPERM' are different findings.\" Keeping them apart, and saying which one\n * happened, is most of this module's job.\n *\n * ## The device-local cache is located and never read\n *\n * `MapsSync_0.0.1_deviceLocalCache.db` sits beside the store with the same 33\n * entities and, on the probed machine, **zero rows in every one of them**. It is\n * reported by diagnostics so anyone comparing the directory against this\n * server's output can see it was considered, and it is never opened.\n */\n\n/** The container path. `Data/Maps` is the gated directory, not `Data/Library`. */\nexport const MAPS_DIR = [\"Library\", \"Containers\", \"com.apple.Maps\", \"Data\", \"Maps\"];\n\n/**\n * No extension, and a version in the name.\n *\n * The `0.0.1` was stable on every machine this project has measured, but it is a\n * version string and it will move. `locateStore` falls back to scanning for a\n * `MapsSync_*` sibling rather than reporting a rename as \"Maps has never run\".\n */\nexport const STORE_FILENAME = \"MapsSync_0.0.1\";\nexport const STORE_PREFIX = \"MapsSync_\";\nexport const LOCAL_CACHE_SUFFIX = \"_deviceLocalCache.db\";\n\nexport const FDA_HINT =\n \"Grant Full Disk Access to the app running this server (System Settings > Privacy & \" +\n \"Security > Full Disk Access) and restart it. Granting it to Maps.app does nothing — the \" +\n \"reader needs the permission, not the app.\";\n\nexport type LocatedFile = FileFacts & { path: string };\n\nexport type LocateResult = StoreFacts & {\n directory: string;\n storePath: string | null;\n /** Reported so a reader can see it was considered. Never opened. */\n localCache: LocatedFile;\n /** True when the name came from a directory scan rather than the constant. */\n resolvedByScan: boolean;\n reason: string | null;\n};\n\nexport const defaultDirectory = (home: string = homedir()): string => join(home, ...MAPS_DIR);\n\nexport const defaultStorePath = (home: string = homedir()): string =>\n join(defaultDirectory(home), STORE_FILENAME);\n\nconst locateFile = (path: string): LocatedFile => ({ ...inspectFile(path), path });\n\n/**\n * Look for a `MapsSync_*` that is neither the local cache nor a SQLite sidecar.\n *\n * Only reached when the constant path is absent, and it needs the directory to\n * be listable — the same grant the store itself needs, so this asks for no new\n * permission and simply survives a version bump.\n */\nconst scanForStore = (directory: string, readdir: (p: string) => string[]): string | null => {\n let names: string[];\n try {\n names = readdir(directory);\n } catch {\n return null;\n }\n const candidate = names\n .filter(\n (n) =>\n n.startsWith(STORE_PREFIX) &&\n !n.endsWith(LOCAL_CACHE_SUFFIX) &&\n !n.endsWith(\"-wal\") &&\n !n.endsWith(\"-shm\"),\n )\n .toSorted()\n .at(-1);\n return candidate ? join(directory, candidate) : null;\n};\n\nexport const locateStore = (\n opts: {\n storePath?: string | undefined;\n home?: string;\n /** Injected by tests so a scan never reaches a real directory. */\n readdir?: (path: string) => string[];\n } = {},\n): LocateResult => {\n const directory = defaultDirectory(opts.home);\n const readdir = opts.readdir ?? ((p: string): string[] => readdirSync(p));\n\n const explicit = opts.storePath;\n const constant = defaultStorePath(opts.home);\n\n let storePath = explicit ?? constant;\n let resolvedByScan = false;\n\n if (!explicit && !describeStore(constant).exists) {\n const scanned = scanForStore(directory, readdir);\n if (scanned) {\n storePath = scanned;\n resolvedByScan = true;\n }\n }\n\n const facts = describeStore(storePath);\n\n const reason = facts.readable\n ? null\n : facts.exists\n ? `Found Maps' store at ${storePath} but cannot read it. ${FDA_HINT}`\n : explicit\n ? `No file at ${explicit}. APPLE_MAPS_STORE points at nothing.`\n : `No Maps store at ${storePath}, and no MapsSync_* file in ${directory}. Either Maps ` +\n `has never been used on this account, or the directory itself is not readable — those ` +\n `look identical from outside and only the grant tells them apart. ${FDA_HINT}`;\n\n return {\n ...facts,\n directory,\n storePath: facts.exists || !explicit ? storePath : null,\n localCache: locateFile(`${constant}${LOCAL_CACHE_SUFFIX}`),\n resolvedByScan,\n reason,\n };\n};\n","import { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\nimport type { EntityKey } from \"./store.js\";\n\n/**\n * Refs for places and collections.\n *\n * ## Why the ref is not `ZMUID`\n *\n * `ZMUID` is Apple's own place identifier and looks like the better key — it is\n * the same number for the same restaurant across every device. It is not used,\n * for two measured reasons recorded in `docs/maps.md`:\n *\n * * It is populated **20 of 23** favourites. The three without it are the\n * rows that have no linked place at all, but a ref scheme that cannot\n * address three rows in twenty-three is not a ref scheme.\n * * It identifies a PLACE, not an entry. The same café saved as a favourite\n * AND filed in a collection carries one `ZMUID` across both, so a ref built\n * on it could not say which of the two a caller meant.\n *\n * The keys below address the ENTRY, which is what every tool here returns. The\n * kind is carried in the prefix so a favourite ref and a collection-item ref\n * can never be confused for one another.\n *\n * ## What the ref carries: a uuid when the store has one, a row id when it does not\n *\n * `ZIDENTIFIER` — a 16-byte Core Data UUID — is set on every favourite,\n * collection, collection item and recent on a real store, and is distinct on\n * every one. It addresses the ENTRY, survives a delete elsewhere in the table,\n * and survives an iCloud re-sync renumbering rows. It is the ref whenever\n * `store.ts` can confirm total coverage.\n *\n * It was found by watching Maps write, not by reading the schema — see\n * `docs/maps.md`. The read probe never reported it, because a probe can only\n * report columns it thought to look for.\n *\n * When a store has no usable identifier the ref falls back to the Core Data row\n * id, and then the old caveat applies in full: `Z_PK` is reused after a delete\n * and renumbered by a re-sync, so such a ref is only good for the session.\n * `packages/messages` rejected row ids outright for that reason; here they are\n * the degraded mode rather than the design.\n *\n * The two are told apart BY SHAPE — a uuid is 32 hex characters, a row id is\n * decimal — and the resolver refuses to try a uuid against a store with no\n * identifier column rather than reinterpreting it as a number. Silently\n * resolving one key space in the other would find a real but wrong place, which\n * is worse than finding nothing.\n *\n * ## Why `p1:` and `pc1:`\n *\n * `c1:` is Calendar's, `r1:` Reminders', `k1:` Contacts', `n1:` Notes',\n * `m1:`/`mc1:` Messages', `s1:`/`sb1:` Safari's. A ref that decodes under two\n * surfaces is worse than one that decodes under none, so each prefix is claimed\n * once and the version digit keeps a future change additive rather than a\n * silent reinterpretation of refs already sitting in a conversation.\n */\n\nexport const PLACE_REF_VERSION = \"p1\";\nexport const COLLECTION_REF_VERSION = \"pc1\";\n\n/** The entity a place ref points into. Carried in the ref, never guessed. */\nexport type PlaceKind = \"favorite\" | \"collection-item\" | \"history\";\n\nconst KIND_CODE: Record<PlaceKind, string> = {\n favorite: \"f\",\n \"collection-item\": \"c\",\n history: \"h\",\n};\nconst CODE_KIND: Record<string, PlaceKind> = {\n f: \"favorite\",\n c: \"collection-item\",\n h: \"history\",\n};\n\nconst PLACE_PATTERN = /^p1:([fch]):([0-9a-f]{32}|\\d+)$/;\nconst COLLECTION_PATTERN = /^pc1:([0-9a-f]{32}|\\d+)$/;\n\n/** 32 hex characters is a uuid; anything else that matched the pattern is a row id. */\nconst toKey = (raw: string): EntityKey =>\n /^[0-9a-f]{32}$/.test(raw)\n ? {\n uuid: `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`,\n }\n : { rowId: Number(raw) };\n\n/** A uuid goes into the ref undashed, so the ref stays one opaque token. */\nconst fromKey = (key: EntityKey): string =>\n \"uuid\" in key ? key.uuid.replaceAll(\"-\", \"\").toLowerCase() : String(key.rowId);\n\nconst otherSurface = (raw: string): string => {\n if (raw.startsWith(\"c1:\")) return \" That one is a Calendar event ref.\";\n if (raw.startsWith(\"r1:\")) return \" That one is a Reminders ref.\";\n if (raw.startsWith(\"k1:\")) return \" That one is a Contacts ref.\";\n if (raw.startsWith(\"n1:\")) return \" That one is a Notes ref.\";\n if (raw.startsWith(\"mc1:\")) return \" That one is a Messages chat ref.\";\n if (raw.startsWith(\"m1:\")) return \" That one is a Messages message ref.\";\n if (raw.startsWith(\"sb1:\")) return \" That one is a Safari bookmark ref.\";\n if (raw.startsWith(\"s1:\")) return \" That one is a Safari history ref.\";\n if (raw.startsWith(\"pc1:\")) return \" That one is a COLLECTION ref — this wants a place ref.\";\n if (raw.startsWith(\"p1:\")) return \" That one is a PLACE ref — this wants a collection ref.\";\n return \"\";\n};\n\nexport class InvalidMapsRefError extends AppleAutomationError {\n override readonly name = \"InvalidMapsRefError\";\n\n constructor(raw: string, want: \"place\" | \"collection\") {\n const shape = want === \"place\" ? '\"p1:<kind>:<id>\"' : '\"pc1:<id>\"';\n super(\n `\"${raw}\" is not a ${want} ref. Refs come from apple_maps_* results and look like ` +\n `${shape} — they are opaque and must not be constructed by hand.${otherSurface(raw)}`,\n { ref: raw },\n );\n }\n}\n\nexport const encodePlaceRef = (kind: PlaceKind, key: EntityKey): string =>\n `${PLACE_REF_VERSION}:${KIND_CODE[kind]}:${fromKey(key)}`;\n\nexport const encodeCollectionRef = (key: EntityKey): string =>\n `${COLLECTION_REF_VERSION}:${fromKey(key)}`;\n\nexport const decodePlaceRef = (raw: string): { kind: PlaceKind; key: EntityKey } => {\n const m = PLACE_PATTERN.exec(raw.trim());\n const kind = m?.[1] ? CODE_KIND[m[1]] : undefined;\n if (!m?.[2] || !kind) throw new InvalidMapsRefError(raw, \"place\");\n return { kind, key: toKey(m[2]) };\n};\n\nexport const decodeCollectionRef = (raw: string): EntityKey => {\n const m = COLLECTION_PATTERN.exec(raw.trim());\n if (!m?.[1]) throw new InvalidMapsRefError(raw, \"collection\");\n return toKey(m[1]);\n};\n","import type { DatabaseSync } from \"node:sqlite\";\n\nimport {\n columnsOf,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n SchemaDriftError,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { resolveEpoch, type Epoch } from \"./dates.js\";\n\n/**\n * Maps' file lane. There is no other lane.\n *\n * ## What is measured, and what is NOT\n *\n * MEASURED by `pnpm probe:maps` on macOS 26.6 — 146 schema objects, fingerprint\n * `2bbc03143125`, Core Data, `ZCREATETIME` on apple-seconds:\n *\n * ZFAVORITEITEM 24 rows ZCOLLECTION 10 rows\n * ZCOLLECTIONITEM 30 rows ZHISTORYITEM 34 rows\n * ZMIXINMAPITEM 71 rows ZREVIEWEDPLACE 24 rows\n *\n * and the id bridge, verified by running the join rather than by reading column\n * names: `ZMIXINMAPITEM`'s inverse relationships partition exactly —\n * 30 collection items + 21 favourites + 20 history rows = 71 = every row.\n *\n * NOT measured: this file has never been run against the real store by its\n * author, who has no Full Disk Access. Everything below is written against a\n * probe REPORT, which is one step further from the data than\n * `packages/safari/src/client/store.ts` was, and that file already treats every\n * column as optional. This one goes further in two ways.\n *\n * ## Columns are resolved BY COVERAGE, not by first name match\n *\n * The probe found `ZHISTORYITEM` carrying both `ZLATITUDE` (1 of 33 rows) and\n * `ZLATITUDE1` (19 of 33). A resolver that took the first candidate it\n * recognised would pick the column that is null 97% of the time and report that\n * Maps holds almost no coordinates. So each logical field names several\n * candidates and the one with the **most non-null values** wins, counted at\n * open time. That is a real query per candidate, run once, on tables of tens of\n * rows.\n *\n * ## Collection membership is a JOIN TABLE, and it is VALIDATED not guessed\n *\n * MEASURED: membership is `Z_6PLACES(Z_6COLLECTIONS, Z_7PLACES)`, a Core Data\n * many-to-many. `Z_PRIMARYKEY` decodes ordinal 6 as Collection and 7 as\n * CollectionItem, so the relationship is `Collection.places`.\n *\n * Four column names were guessed here before it was found — ZCOLLECTION,\n * ZCOLLECTION1, ZPARENTCOLLECTION, ZOWNINGCOLLECTION — and the store has none\n * of them, so every guide listed empty. A many-to-many leaves NO COLUMN on\n * either entity, so no list of column names could have contained the answer.\n * The near-miss is what makes this worth stating: `ZCOLLECTIONITEM.ZMAPITEM`\n * joins `ZCOLLECTION` for 3 of 10 collections, and a resolver picking the\n * best-covered joinable column would have chosen it and been confidently wrong.\n *\n * So membership is resolved BY RUNNING THE JOIN and scoring it against an\n * oracle the store hands over for free: `ZCOLLECTION.ZPLACESCOUNT`, Maps' own\n * count per guide. A candidate is accepted only when it reproduces all ten\n * numbers exactly with no key pointing at a missing collection. That survives\n * Apple renaming the relationship, which a hard-coded `Z_6PLACES` would not.\n *\n * The oracle is not independent evidence, and it is stronger for it. Core Data\n * maintains `ZPLACESCOUNT` with a trigger that reads:\n *\n * UPDATE ZCOLLECTION SET ZPLACESCOUNT =\n * (SELECT IFNULL(COUNT(Z_6COLLECTIONS), 0)\n * FROM Z_6PLACES WHERE Z_6COLLECTIONS = NEW.Z_PK)\n *\n * so Apple's own schema states the relationship this resolver re-derives. The\n * count match is therefore guaranteed for the true mechanism rather than lucky,\n * and coincidental for anything else. The triggers are visible only in the\n * captured fixture — `pnpm probe:maps --write` omits them from the replay\n * because they call Core Data's private SQLite functions, which is recorded in\n * `writeFixture`.\n *\n * Because it is many-to-many, one place can sit in several guides, and the\n * membership query uses `IN (SELECT ...)` rather than a JOIN so a place in two\n * guides is not returned twice from one of them.\n *\n * MEASURED: 30 item rows, 18 of them in a guide. The other 12 belong to no\n * collection at all; all 30 link to a `ZMIXINMAPITEM`, so they are intact\n * places rather than broken rows.\n *\n * `SchemaDriftError` fires for exactly one condition — none of the three\n * place-bearing tables exists — because without them there is no surface.\n * Everything else is a capability downgrade that is reported, never a throw.\n */\n\nexport const FAVORITES_TABLE = \"ZFAVORITEITEM\";\nexport const COLLECTIONS_TABLE = \"ZCOLLECTION\";\nexport const COLLECTION_ITEMS_TABLE = \"ZCOLLECTIONITEM\";\nexport const HISTORY_TABLE = \"ZHISTORYITEM\";\nexport const MAP_ITEMS_TABLE = \"ZMIXINMAPITEM\";\n\n/**\n * Candidates per logical field, best-known first — but order only breaks ties.\n * Coverage decides. See the header.\n */\nconst NAME_CANDIDATES = [\"ZMAPITEMNAME\", \"ZCUSTOMNAME\", \"ZLOCATIONDISPLAY\", \"ZTITLE\"] as const;\nconst LAT_CANDIDATES = [\"ZLATITUDE\", \"ZLATITUDE1\"] as const;\nconst LON_CANDIDATES = [\"ZLONGITUDE\", \"ZLONGITUDE1\"] as const;\nconst ADDRESS_CANDIDATES = [\"ZMAPITEMADDRESS\", \"ZORIGINATINGADDRESSSTRING\"] as const;\nconst MUID_CANDIDATES = [\"ZMUID\"] as const;\nconst MAP_ITEM_FK_CANDIDATES = [\"ZMAPITEM\"] as const;\nconst CREATED_CANDIDATES = [\"ZCREATETIME\"] as const;\nconst MODIFIED_CANDIDATES = [\"ZMODIFICATIONTIME\"] as const;\nconst COLLECTION_TITLE_CANDIDATES = [\"ZTITLE\", \"ZNAME\", \"ZCUSTOMNAME\"] as const;\n\n/**\n * The stable per-entry identifier, if this store has one.\n *\n * FOUND BY WATCHING MAPS WRITE, not by reading the schema: every row Maps\n * created during `pnpm probe:maps-write` carried `ZIDENTIFIER`, a 16-byte blob\n * — a UUID. It is not in the read probe's report because that probe only looked\n * for columns it already had candidates for, which is a good argument for\n * diffing a live write even when the read surface already works.\n *\n * `ZORIGINALIDENTIFIER` is deliberately NOT a candidate. It exists on\n * `ZCOLLECTIONITEM` and is populated on 3 rows of 30 — it records where an entry\n * was copied FROM, so it is neither complete nor unique to the entry.\n */\nconst IDENTIFIER_CANDIDATES = [\"ZIDENTIFIER\"] as const;\n\n/**\n * Maps' own count of the places in a guide, and the oracle every membership\n * candidate is scored against. See the header.\n */\nconst COLLECTION_COUNT_COLUMN = \"ZPLACESCOUNT\";\n\n/** Core Data's own tables, which are never a relationship. */\nconst RESERVED_JOIN_TABLES = new Set([\"Z_METADATA\", \"Z_MODELCACHE\", \"Z_PRIMARYKEY\"]);\n\nexport type FieldMap = {\n /**\n * The column holding a stable UUID, or null when this store has none good\n * enough to address rows by. See `resolveIdentifier`.\n */\n identifier: string | null;\n name: string | null;\n customName: string | null;\n latitude: string | null;\n longitude: string | null;\n address: string | null;\n muid: string | null;\n mapItem: string | null;\n created: string | null;\n modified: string | null;\n};\n\nexport type EntityFacts = {\n table: string;\n present: boolean;\n rows: number;\n columns: Set<string>;\n fields: FieldMap;\n};\n\n/**\n * How an item says which collection it is in, once proved against the oracle.\n *\n * Two shapes because Core Data has two: a scalar foreign key named after the\n * relationship, and a `Z_<ordinal><RELATIONSHIP>` join table for a\n * many-to-many. THIS store uses the second. Both are carried because the\n * resolver proves whichever is there rather than assuming, and a store that\n * changes shape should degrade rather than read the wrong column.\n */\nexport type CollectionMembership =\n | { kind: \"column\"; column: string }\n | { kind: \"joinTable\"; table: string; collectionColumn: string; itemColumn: string };\n\nexport type StoreCapabilities = {\n fingerprint: string;\n tables: string[];\n favorites: EntityFacts;\n collections: EntityFacts;\n collectionItems: EntityFacts;\n history: EntityFacts;\n mapItems: EntityFacts;\n /**\n * The proved membership mechanism, or null when nothing reproduced\n * `ZPLACESCOUNT` and collections must therefore list without their places.\n */\n membership: CollectionMembership | null;\n /**\n * The membership mechanism as one short string, for diagnostics.\n *\n * Kept because `apple_maps_diagnostics` reported `collectionFk` before this\n * was understood, and a field that silently changes meaning is worse than one\n * that widens: it now names a join table as well as a column.\n */\n collectionFk: string | null;\n epoch: Epoch;\n};\n\nexport type PlaceRow = {\n id: number;\n /**\n * The stable identifier, when the store has one. Null means refs on this\n * entity fall back to the row id and are only good for the session.\n */\n uuid: string | null;\n name: string | null;\n customName: string | null;\n latitude: number | null;\n longitude: number | null;\n address: string | null;\n /**\n * Apple's place id, carried as a STRING.\n *\n * MEASURED on a real store: `-2679868148951248105`. It is a 64-bit integer and\n * `node:sqlite` THROWS on one past `Number.MAX_SAFE_INTEGER` rather than\n * truncating, so reading it as a number failed the whole listing with\n * \"Value is too large to be represented as a JavaScript number\" — one column\n * taking down every favourite. `docs/surfaces.md` states the rule this broke:\n * read such columns as BigInt or `CAST(... AS TEXT)`.\n *\n * The fixture used a small id, so the offline suite passed. Only the real\n * store has ids of this size.\n */\n muid: string | null;\n createdRaw: number | null;\n modifiedRaw: number | null;\n /**\n * False when the row has no linked `ZMIXINMAPITEM`.\n *\n * MEASURED: 3 of 23 favourites, and they carry no name and no coordinate\n * either. Almost certainly the unconfigured Home / Work / School slots that\n * Maps creates whether or not anyone fills them in. They are RETURNED rather\n * than filtered, with this flag, because silently dropping rows is how a\n * caller concludes a favourite was deleted.\n */\n linked: boolean;\n};\n\nexport type CollectionRow = {\n id: number;\n uuid: string | null;\n title: string | null;\n placesCount: number | null;\n createdRaw: number | null;\n modifiedRaw: number | null;\n};\n\n/**\n * How a caller addresses one row.\n *\n * Two shapes rather than one because the store decides which is available, not\n * the caller: a store whose `ZIDENTIFIER` is complete gets durable refs, one\n * without falls back to row ids. Making the difference explicit in the type\n * means a resolver cannot quietly treat a row id as a uuid when the store\n * changed underneath it.\n */\nexport type EntityKey = { uuid: string } | { rowId: number };\n\nconst isRealTable = (name: string): boolean => !name.startsWith(\"sqlite_\");\n\n/**\n * `HEX()` of a 16-byte blob to a canonical UUID.\n *\n * Returns null on anything that is not exactly 32 hex characters. Core Data\n * stores a UUID attribute as 16 raw bytes, so a different length means the\n * column is not what this code thinks it is — and a malformed ref that still\n * looks ref-shaped would be resolved against the wrong row rather than rejected.\n */\nconst toUuid = (hex: unknown): string | null => {\n if (hex === null || hex === undefined) return null;\n const h = String(hex).toLowerCase();\n if (!/^[0-9a-f]{32}$/.test(h)) return null;\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n};\n\n/** A `NULL AS alias` column reads back as null; every other value is coerced. */\nconst num = (v: unknown): number | null =>\n v === null || v === undefined ? null : Number(v as number);\nconst str = (v: unknown): string | null =>\n v === null || v === undefined ? null : String(v as string);\n\n/**\n * Pick the candidate column with the most non-null values.\n *\n * Returns null when none of the candidates exists, and also when they all exist\n * and are all empty — an all-null column is not a usable field, and reporting\n * one as resolved would produce a result whose every value is null with no\n * explanation.\n */\nconst resolveField = (\n db: DatabaseSync,\n table: string,\n columns: Set<string>,\n candidates: readonly string[],\n rows: number,\n): string | null => {\n const present = candidates.filter((c) => columns.has(c));\n if (present.length === 0) return null;\n if (rows === 0) return present[0] ?? null;\n let best: { name: string; count: number } | null = null;\n for (const name of present) {\n let count = 0;\n try {\n // COUNT() is always small. The VALUES are not — see PlaceRow.muid — so\n // nothing here ever selects the column itself.\n count = Number(\n (\n db.prepare(`SELECT COUNT(\"${name}\") AS c FROM \"${table}\"`).get() as {\n c: number | bigint;\n }\n ).c,\n );\n } catch {\n continue;\n }\n if (!best || count > best.count) best = { name, count };\n }\n return best && best.count > 0 ? best.name : (present[0] ?? null);\n};\n\n/**\n * The identifier column, but ONLY if it can actually carry every ref.\n *\n * Coverage is not enough here, and this is the difference between a fix and a\n * trap. A column populated on the rows Maps has written lately and null on the\n * ones that predate it would work perfectly for every place the user saves from\n * now on and fail silently for the places they already had — the failure mode\n * hardest to notice and worst to hit, since the older entries are the ones a\n * person is most likely to ask about.\n *\n * So the bar is TOTAL: set on every row, and distinct across every row. Anything\n * less returns null and the store falls back to `Z_PK`, which is worse but\n * uniformly worse. Measured on a real store: 24/24 favourites, 30/30 collection\n * items, 10/10 collections, 33/33 recents, all distinct.\n */\nconst resolveIdentifier = (\n db: DatabaseSync,\n table: string,\n columns: Set<string>,\n rows: number,\n): string | null => {\n const present = IDENTIFIER_CANDIDATES.filter((c) => columns.has(c));\n if (present.length === 0 || rows === 0) return null;\n for (const name of present) {\n try {\n const r = db\n .prepare(\n `SELECT COUNT(\"${name}\") AS nset, COUNT(DISTINCT \"${name}\") AS ndistinct FROM \"${table}\"`,\n )\n .get() as { nset: number | bigint; ndistinct: number | bigint };\n if (Number(r.nset) === rows && Number(r.ndistinct) === rows) return name;\n } catch {\n continue;\n }\n }\n return null;\n};\n\nconst countRows = (db: DatabaseSync, table: string): number => {\n try {\n return Number(\n (db.prepare(`SELECT COUNT(*) AS c FROM \"${table}\"`).get() as { c: number | bigint }).c,\n );\n } catch {\n return 0;\n }\n};\n\nconst describeEntity = (\n db: DatabaseSync,\n table: string,\n tables: string[],\n nameCandidates: readonly string[] = NAME_CANDIDATES,\n): EntityFacts => {\n const present = tables.includes(table);\n if (!present) {\n return {\n table,\n present: false,\n rows: 0,\n columns: new Set(),\n fields: {\n identifier: null,\n name: null,\n customName: null,\n latitude: null,\n longitude: null,\n address: null,\n muid: null,\n mapItem: null,\n created: null,\n modified: null,\n },\n };\n }\n const columns = new Set(columnsOf(db, table));\n const rows = countRows(db, table);\n const pick = (cands: readonly string[]): string | null =>\n resolveField(db, table, columns, cands, rows);\n return {\n table,\n present,\n rows,\n columns,\n fields: {\n identifier: resolveIdentifier(db, table, columns, rows),\n name: pick(nameCandidates),\n // Always named directly: it is the user's own label and must never be\n // confused with the place's name, even when it wins on coverage.\n customName: columns.has(\"ZCUSTOMNAME\") ? \"ZCUSTOMNAME\" : null,\n latitude: pick(LAT_CANDIDATES),\n longitude: pick(LON_CANDIDATES),\n address: pick(ADDRESS_CANDIDATES),\n muid: pick(MUID_CANDIDATES),\n mapItem: pick(MAP_ITEM_FK_CANDIDATES),\n created: pick(CREATED_CANDIDATES),\n modified: pick(MODIFIED_CANDIDATES),\n },\n };\n};\n\n/**\n * Maps' own places-per-guide, keyed by collection row id.\n *\n * Null when the store has no `ZPLACESCOUNT`, which changes what the resolver\n * below is allowed to accept — see `resolveMembership`.\n */\nconst declaredCounts = (db: DatabaseSync, collections: EntityFacts): Map<number, number> | null => {\n if (!collections.present || !collections.columns.has(COLLECTION_COUNT_COLUMN)) return null;\n try {\n const rows = db\n .prepare(`SELECT \"Z_PK\" AS pk, \"${COLLECTION_COUNT_COLUMN}\" AS n FROM \"${collections.table}\"`)\n .all() as { pk: number | bigint; n: number | bigint | null }[];\n return new Map(rows.map((r) => [Number(r.pk), Number(r.n ?? 0)]));\n } catch {\n return null;\n }\n};\n\n/**\n * Group a candidate's keys and count the items under each.\n *\n * Keys are read as TEXT because `ZMUID` is an INTEGER column holding 64-bit\n * place ids, and `node:sqlite` THROWS on those rather than truncating — reading\n * raw would drop the candidate into a catch instead of rejecting it on the\n * evidence. See `PlaceRow.muid`.\n */\nconst tally = (db: DatabaseSync, sql: string): Map<number, number> => {\n try {\n const rows = db.prepare(sql).all() as { pk: string | null; n: number | bigint }[];\n return new Map(\n rows.filter((r) => r.pk !== null).map((r) => [Number(r.pk), Number(r.n)] as const),\n );\n } catch {\n return new Map();\n }\n};\n\n/**\n * Does this candidate reproduce Maps' own counts, exactly, for every guide?\n *\n * Exactness is the whole test. `ZCOLLECTIONITEM.ZMAPITEM` matches 3 of 10\n * collections by coincidence, so \"mostly right\" is precisely the answer that\n * must be rejected. A key pointing at no collection at all (`unknown`) is\n * disqualifying for the same reason.\n */\nconst reproducesCounts = (declared: Map<number, number>, tallies: Map<number, number>): boolean => {\n for (const pk of tallies.keys()) if (!declared.has(pk)) return false;\n for (const [pk, n] of declared) if ((tallies.get(pk) ?? 0) !== n) return false;\n return true;\n};\n\n/**\n * Find how an item belongs to a collection, by running each candidate join.\n *\n * Scalar columns are tried before join tables only so the cheaper query runs\n * first; the verdict does not depend on order, because a candidate is accepted\n * only when it reproduces every count. When several would qualify the first is\n * taken, and that ambiguity cannot arise on a store whose counts are distinct.\n *\n * WITHOUT the oracle the bar changes rather than disappearing: a single join\n * table whose every key resolves on both sides is accepted, and anything\n * ambiguous is refused. That is weaker evidence, and it is the reason the\n * result is reported to callers rather than assumed.\n */\nconst resolveMembership = (\n db: DatabaseSync,\n collectionItems: EntityFacts,\n collections: EntityFacts,\n tables: string[],\n): CollectionMembership | null => {\n if (!collectionItems.present || !collections.present) return null;\n const declared = declaredCounts(db, collections);\n\n if (declared) {\n for (const column of collectionItems.columns) {\n if ([\"Z_PK\", \"Z_ENT\", \"Z_OPT\"].includes(column)) continue;\n const tallies = tally(\n db,\n `SELECT CAST(t.\"${column}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${collectionItems.table}\" t\n WHERE t.\"${column}\" IS NOT NULL GROUP BY t.\"${column}\"`,\n );\n if (tallies.size > 0 && reproducesCounts(declared, tallies))\n return { kind: \"column\", column };\n }\n }\n\n const joinTables = tables.filter((t) => t.startsWith(\"Z_\") && !RESERVED_JOIN_TABLES.has(t));\n const accepted: CollectionMembership[] = [];\n for (const table of joinTables) {\n const columns = columnsOf(db, table);\n // Both orientations: the column names do not say which side is which, and\n // reading `Z_6COLLECTIONS` as the collection side is an inference, not a\n // fact. On the real store the wrong orientation scores 3 of 10.\n for (const collectionColumn of columns) {\n for (const itemColumn of columns) {\n if (collectionColumn === itemColumn) continue;\n const tallies = tally(\n db,\n `SELECT CAST(j.\"${collectionColumn}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${table}\" j\n JOIN \"${collectionItems.table}\" t ON t.\"Z_PK\" = j.\"${itemColumn}\"\n GROUP BY j.\"${collectionColumn}\"`,\n );\n if (tallies.size === 0) continue;\n if (declared) {\n if (reproducesCounts(declared, tallies))\n return { kind: \"joinTable\", table, collectionColumn, itemColumn };\n continue;\n }\n // No oracle: demand that every key on both sides resolves to a real row.\n const orphaned = tally(\n db,\n `SELECT CAST(j.\"${collectionColumn}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${table}\" j\n LEFT JOIN \"${collections.table}\" c ON c.\"Z_PK\" = j.\"${collectionColumn}\"\n WHERE c.\"Z_PK\" IS NULL GROUP BY j.\"${collectionColumn}\"`,\n );\n if (orphaned.size === 0)\n accepted.push({ kind: \"joinTable\", table, collectionColumn, itemColumn });\n }\n }\n }\n // Exactly one unambiguous reading, or nothing. Two candidates mean the\n // evidence does not distinguish them, and picking one would be a guess.\n return accepted.length === 1 ? (accepted[0] ?? null) : null;\n};\n\n/** The mechanism as one short string, for diagnostics. */\nconst describeMembership = (m: CollectionMembership | null): string | null => {\n if (!m) return null;\n return m.kind === \"column\" ? m.column : `${m.table}(${m.collectionColumn}, ${m.itemColumn})`;\n};\n\nexport const introspect = (db: DatabaseSync, now: number = Date.now()): StoreCapabilities => {\n const tables = (\n db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`).all() as {\n name: string;\n }[]\n )\n .map((r) => r.name)\n .filter(isRealTable);\n\n const favorites = describeEntity(db, FAVORITES_TABLE, tables);\n const collectionItems = describeEntity(db, COLLECTION_ITEMS_TABLE, tables);\n const history = describeEntity(db, HISTORY_TABLE, tables);\n\n if (!favorites.present && !collectionItems.present && !history.present) {\n throw new SchemaDriftError(\n `Maps' store has none of \"${FAVORITES_TABLE}\", \"${COLLECTION_ITEMS_TABLE}\" or ` +\n `\"${HISTORY_TABLE}\" (found: ${tables.join(\", \") || \"nothing\"}). This is either not a ` +\n `MapsSync store or Apple has restructured it. Nothing can be read from it.`,\n );\n }\n\n const collections = describeEntity(db, COLLECTIONS_TABLE, tables, COLLECTION_TITLE_CANDIDATES);\n const mapItems = describeEntity(db, MAP_ITEMS_TABLE, tables);\n\n const membership = resolveMembership(db, collectionItems, collections, tables);\n\n // The epoch comes from whichever place table has the most rows and a created\n // column — one measurement for the whole store, since every entity is written\n // by the same framework.\n const dated = [favorites, collectionItems, history, mapItems]\n .filter((e) => e.present && e.rows > 0 && e.fields.created)\n .toSorted((a, b) => b.rows - a.rows)[0];\n let maxCreated: number | null = null;\n if (dated?.fields.created) {\n try {\n maxCreated = (\n db\n .prepare(`SELECT MAX(CAST(\"${dated.fields.created}\" AS REAL)) AS m FROM \"${dated.table}\"`)\n .get() as { m: number | null }\n ).m;\n } catch {\n maxCreated = null;\n }\n }\n\n return {\n fingerprint: fingerprintSchema(db),\n tables,\n favorites,\n collections,\n collectionItems,\n history,\n mapItems,\n membership,\n collectionFk: describeMembership(membership),\n epoch: resolveEpoch(maxCreated, now),\n };\n};\n\nexport class MapsStore {\n readonly db: DatabaseSync;\n readonly caps: StoreCapabilities;\n readonly path: string;\n readonly mode: ReadOnlyMode;\n\n constructor(opts: {\n db: DatabaseSync;\n caps: StoreCapabilities;\n path: string;\n mode: ReadOnlyMode;\n }) {\n this.db = opts.db;\n this.caps = opts.caps;\n this.path = opts.path;\n this.mode = opts.mode;\n }\n\n /**\n * One column, or `NULL` under the same alias when it could not be resolved.\n *\n * The same guard `packages/safari` uses, and needed more here: this schema was\n * read from a probe report rather than from the database, so a wrong\n * expectation should cost one field instead of the whole lane.\n */\n #col(column: string | null, alias: string, table = \"t\"): string {\n return column ? `${table}.\"${column}\" AS ${alias}` : `NULL AS ${alias}`;\n }\n\n /**\n * The same, for a column whose value may not fit in a JS double.\n *\n * SQLite holds 64-bit integers; a JS number holds 53 bits of them, and\n * `node:sqlite` refuses rather than silently losing precision. Casting in SQL\n * keeps the exact value and moves the decision about what to do with it out\n * of the driver.\n */\n #colText(column: string | null, alias: string, table = \"t\"): string {\n return column ? `CAST(${table}.\"${column}\" AS TEXT) AS ${alias}` : `NULL AS ${alias}`;\n }\n\n #placeSelect(e: EntityFacts): string {\n const f = e.fields;\n return [\n `t.\"Z_PK\" AS id`,\n f.identifier ? `HEX(t.\"${f.identifier}\") AS uuid` : `NULL AS uuid`,\n this.#col(f.name, \"name\"),\n this.#col(f.customName, \"customName\"),\n this.#col(f.latitude, \"latitude\"),\n this.#col(f.longitude, \"longitude\"),\n this.#col(f.address, \"address\"),\n this.#colText(f.muid, \"muid\"),\n this.#col(f.created, \"createdRaw\"),\n this.#col(f.modified, \"modifiedRaw\"),\n f.mapItem ? `t.\"${f.mapItem}\" AS mapItem` : `NULL AS mapItem`,\n ].join(\", \");\n }\n\n #toPlace(r: Record<string, unknown>): PlaceRow {\n return {\n id: Number(r.id),\n uuid: toUuid(r.uuid),\n name: str(r.name),\n customName: str(r.customName),\n latitude: num(r.latitude),\n longitude: num(r.longitude),\n address: str(r.address),\n // MEASURED on a real store: 12 of 20 linked favourites carry ZMUID = 0.\n // Zero is a sentinel for \"no Apple place id\", not an id, and reporting it\n // as one invites a caller to treat two unrelated places as the same\n // place. Only 8 of the 20 have a real id, and all 8 are 19 digits.\n muid:\n r.muid === null || r.muid === undefined || String(r.muid) === \"0\" ? null : String(r.muid),\n createdRaw: num(r.createdRaw),\n modifiedRaw: num(r.modifiedRaw),\n linked: r.mapItem !== null && r.mapItem !== undefined,\n };\n }\n\n #entityFor(kind: \"favorite\" | \"collection-item\" | \"history\"): EntityFacts {\n if (kind === \"favorite\") return this.caps.favorites;\n if (kind === \"collection-item\") return this.caps.collectionItems;\n return this.caps.history;\n }\n\n /**\n * Items belonging to no collection at all, or null when unanswerable.\n *\n * MEASURED: 30 collection items, 18 filed in a guide, 12 in none — and Core\n * Data has deleted nothing (`Z_PRIMARYKEY.Z_MAX` equals the live count for\n * both `Collection` and `CollectionItem`), so these are not the debris of\n * removed guides. 7 of the 12 appear nowhere else in the store: not as a\n * favourite, not in another guide, not in recents. They are places the user\n * saved that no other tool can reach.\n *\n * `NOT IN` needs the null guard. A subquery yielding a single NULL makes\n * `NOT IN` false for EVERY row, so the filter would silently return nothing\n * and read as \"you have no unfiled places\" — the failure this whole surface\n * keeps having to design against.\n */\n #unfiledClause(): string | null {\n const m = this.caps.membership;\n if (!m) return null;\n if (m.kind === \"column\") return `t.\"${m.column}\" IS NULL`;\n return (\n `t.\"Z_PK\" NOT IN (SELECT j.\"${m.itemColumn}\" FROM \"${m.table}\" j ` +\n `WHERE j.\"${m.itemColumn}\" IS NOT NULL)`\n );\n }\n\n /** How many collection items are filed in no collection; null when unknown. */\n unfiledCount(): number | null {\n const e = this.caps.collectionItems;\n const clause = this.#unfiledClause();\n if (!e.present || !clause) return null;\n try {\n return Number(\n (\n this.db.prepare(`SELECT COUNT(*) AS c FROM \"${e.table}\" t WHERE ${clause}`).get() as {\n c: number | bigint;\n }\n ).c,\n );\n } catch {\n return null;\n }\n }\n\n /** One entity's places, newest first when a date is available. */\n places(\n kind: \"favorite\" | \"collection-item\" | \"history\",\n opts: {\n limit: number;\n collectionId?: number | undefined;\n query?: string | undefined;\n /** Only items filed in NO collection. See `#unfiledClause`. */\n unfiled?: boolean | undefined;\n },\n ): { rows: PlaceRow[]; truncated: boolean } {\n const e = this.#entityFor(kind);\n if (!e.present || e.rows === 0) return { rows: [], truncated: false };\n\n const where: string[] = [];\n const params: (string | number)[] = [];\n\n if (opts.collectionId !== undefined) {\n const m = this.caps.membership;\n // Nothing reproduced ZPLACESCOUNT, so which places are in this guide is\n // genuinely unknown. Empty is the honest answer and callers are told why.\n if (!m) return { rows: [], truncated: false };\n if (m.kind === \"column\") {\n where.push(`t.\"${m.column}\" = ?`);\n } else {\n // IN, not JOIN, because the question is set membership: is this item\n // in this guide. A JOIN also multiplies the row by the number of\n // matching join rows, which is 1 today only because Core Data gives the\n // table a compound primary key — a property of the schema rather than\n // of the query. IN does not depend on it.\n where.push(\n `t.\"Z_PK\" IN (SELECT j.\"${m.itemColumn}\" FROM \"${m.table}\" j ` +\n `WHERE j.\"${m.collectionColumn}\" = ?)`,\n );\n }\n params.push(opts.collectionId);\n }\n\n if (opts.unfiled) {\n const clause = this.#unfiledClause();\n // Membership is unresolved, so \"in no collection\" is not a question this\n // store can answer either. Empty, and the caller is told why.\n if (!clause) return { rows: [], truncated: false };\n where.push(clause);\n }\n\n if (opts.query) {\n const pattern = `%${escapeLike(opts.query)}%`;\n const clauses: string[] = [];\n for (const col of [e.fields.name, e.fields.customName, e.fields.address]) {\n if (!col) continue;\n clauses.push(`t.\"${col}\" LIKE ? ESCAPE '\\\\'`);\n params.push(pattern);\n }\n // No searchable column means no rows can match. Returning everything\n // would be worse than returning nothing: it reads as a successful search.\n if (clauses.length === 0) return { rows: [], truncated: false };\n where.push(`(${clauses.join(\" OR \")})`);\n }\n\n const limit = Math.max(1, opts.limit);\n params.push(limit + 1);\n\n const order = e.fields.modified\n ? `t.\"${e.fields.modified}\" DESC`\n : e.fields.created\n ? `t.\"${e.fields.created}\" DESC`\n : `t.\"Z_PK\" DESC`;\n\n const sql =\n `SELECT ${this.#placeSelect(e)} FROM \"${e.table}\" t ` +\n (where.length ? `WHERE ${where.join(\" AND \")} ` : \"\") +\n `ORDER BY ${order} LIMIT ?`;\n\n const raw = this.db.prepare(sql).all(...params) as Record<string, unknown>[];\n return {\n rows: raw.slice(0, limit).map((r) => this.#toPlace(r)),\n truncated: raw.length > limit,\n };\n }\n\n /**\n * The WHERE clause and parameter for one key, or null when the key cannot be\n * honoured against this entity.\n *\n * A uuid key against a store with no resolved identifier column returns null\n * rather than falling back to the row id. The two number spaces are unrelated,\n * so a fallback would resolve to a real but WRONG place — the one outcome\n * worse than not finding it.\n *\n * `HEX()` on both sides rather than `X'..'` literal: it keeps the comparison\n * in one form, and these tables are tens of rows, so the lost index is free.\n */\n #keyClause(e: EntityFacts, key: EntityKey): { sql: string; param: string | number } | null {\n if (\"uuid\" in key) {\n if (!e.fields.identifier) return null;\n return {\n sql: `HEX(t.\"${e.fields.identifier}\") = ?`,\n param: key.uuid.replaceAll(\"-\", \"\").toUpperCase(),\n };\n }\n return { sql: `t.\"Z_PK\" = ?`, param: key.rowId };\n }\n\n /** One place by entity and key. */\n place(kind: \"favorite\" | \"collection-item\" | \"history\", key: EntityKey): PlaceRow | null {\n const e = this.#entityFor(kind);\n if (!e.present) return null;\n const clause = this.#keyClause(e, key);\n if (!clause) return null;\n const sql = `SELECT ${this.#placeSelect(e)} FROM \"${e.table}\" t WHERE ${clause.sql} LIMIT 1`;\n const r = this.db.prepare(sql).get(clause.param) as Record<string, unknown> | undefined;\n return r ? this.#toPlace(r) : null;\n }\n\n /**\n * A collection key to the row id its items point at.\n *\n * Collection membership is a Core Data foreign key, so it holds `Z_PK` values\n * whatever the ref carries. A uuid ref has to be translated before it can\n * filter items, and this is the one place that happens.\n */\n collectionRowId(key: EntityKey): number | null {\n if (!(\"uuid\" in key)) return key.rowId;\n const e = this.caps.collections;\n const clause = this.#keyClause(e, key);\n if (!e.present || !clause) return null;\n const r = this.db\n .prepare(`SELECT t.\"Z_PK\" AS id FROM \"${e.table}\" t WHERE ${clause.sql} LIMIT 1`)\n .get(clause.param) as { id: number | bigint } | undefined;\n return r ? Number(r.id) : null;\n }\n\n collections(opts: { limit: number }): { rows: CollectionRow[]; truncated: boolean } {\n const e = this.caps.collections;\n if (!e.present || e.rows === 0) return { rows: [], truncated: false };\n const limit = Math.max(1, opts.limit);\n const hasCount = e.columns.has(\"ZPLACESCOUNT\");\n const order = e.fields.modified ? `t.\"${e.fields.modified}\" DESC` : `t.\"Z_PK\" DESC`;\n const sql =\n `SELECT t.\"Z_PK\" AS id, ` +\n `${e.fields.identifier ? `HEX(t.\"${e.fields.identifier}\") AS uuid` : `NULL AS uuid`}, ` +\n `${this.#col(e.fields.name, \"title\")}, ` +\n `${hasCount ? `t.\"ZPLACESCOUNT\" AS placesCount` : `NULL AS placesCount`}, ` +\n `${this.#col(e.fields.created, \"createdRaw\")}, ` +\n `${this.#col(e.fields.modified, \"modifiedRaw\")} ` +\n `FROM \"${e.table}\" t ORDER BY ${order} LIMIT ?`;\n const raw = this.db.prepare(sql).all(limit + 1) as Record<string, unknown>[];\n return {\n rows: raw.slice(0, limit).map((r) => ({\n id: Number(r.id),\n uuid: toUuid(r.uuid),\n title: r.title === null || r.title === undefined ? null : String(r.title),\n placesCount: r.placesCount === null ? null : Number(r.placesCount),\n createdRaw: r.createdRaw === null ? null : Number(r.createdRaw),\n modifiedRaw: r.modifiedRaw === null ? null : Number(r.modifiedRaw),\n })),\n truncated: raw.length > limit,\n };\n }\n\n close(): void {\n try {\n this.db.close();\n } catch {\n // Already closed, or the handle died with the process. Nothing to do.\n }\n }\n}\n\nexport const openStore = (opts: {\n path: string;\n mode?: ReadOnlyMode;\n logger?: Logger;\n now?: number;\n hint?: string;\n}): MapsStore => {\n const opened = openReadOnly(opts.path, opts.mode ?? \"auto\", {\n envVar: \"APPLE_MAPS_INDEX_MODE\",\n label: \"Maps' place store\",\n ...(opts.hint ? { hint: opts.hint } : {}),\n validate: (db) => introspect(db, opts.now),\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n opts.logger?.warn?.(\n \"maps: opened immutable; places saved since the last checkpoint may be missing\",\n ),\n });\n return new MapsStore({\n db: opened.db,\n caps: opened.validated,\n path: opts.path,\n mode: opened.mode,\n });\n};\n","import { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { DatabaseSync } from \"node:sqlite\";\n\nimport { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\nimport { CORE_DATA_EPOCH_OFFSET } from \"./dates.js\";\nimport type { EntityKey } from \"./store.js\";\n\n/**\n * Writing to Maps' store.\n *\n * ## Why this is SQL and not the app's own API\n *\n * Maps ships no scripting dictionary and registers no App Intents on macOS, so\n * there is no lane where the app performs the write on our behalf. Every other\n * surface in this repo writes through an Apple Event; this one cannot, and the\n * alternatives were measured rather than assumed — see `docs/maps.md`.\n *\n * ## The one thing that cannot be synthesised, and how it is obtained anyway\n *\n * A place is only real to Maps if it has a `ZMAPITEMSTORAGE` — a GEO protobuf of\n * one to four kilobytes that this repo has never decoded and cannot generate.\n *\n * It does not have to. **Opening a place makes Maps write one.**\n * `maps://?q=<name>&ll=<lat>,<lon>` goes through LaunchServices — not Apple\n * Events, not Accessibility — and Maps resolves the place and records it in\n * Recents with a full record attached. That record is then copied into the new\n * favourite. The blob is always minted by Maps, which is the only thing that can\n * mint one correctly.\n *\n * MEASURED consequences of that design, all of them in `docs/maps.md`:\n *\n * * The insert needs THREE tables — the favourite, its map item, and the two\n * `Z_PRIMARYKEY` counters. Not the eight the app touches.\n * * No persistent history and no `NSCK*` metadata is written. Core Data\n * reconciles unregistered objects on the app's next save and mirrors them to\n * iCloud by itself; a favourite written this way reached another device.\n * * It works with Maps RUNNING, and survives a subsequent save by the app.\n * There is no need to quit anything.\n *\n * ## The cost the caller must be told about\n *\n * Seeding leaves the place in the user's **Recents**, whether or not the\n * favourite is kept. That is unavoidable — it is the mechanism — so every tool\n * built on this says so in its description rather than springing it on somebody.\n *\n * ## Why writes are dangerous here in a way the other surfaces are not\n *\n * The store is mirrored by `NSPersistentCloudKitContainer`, and mirroring does\n * not wait to be told. A malformed row is not a local mistake: it reaches every\n * device on the account as soon as Maps next runs. There is no such thing as a\n * local-only insert here — that was believed for a while and measured false.\n * Hence: never fabricate a place record, only ever copy one Maps wrote.\n */\n\nconst STORAGE_POLL_MS = 250;\n\n/**\n * Progress, on stderr.\n *\n * Adding a favourite can take tens of seconds — Maps has to resolve a place over\n * the network before there is anything to copy — and the first version said\n * NOTHING for the whole of it. That is indistinguishable from a hang, and was\n * reported as one three times while three different theories were tried. A long\n * operation that reports nothing cannot be diagnosed, only guessed at.\n *\n * stderr because this is an MCP stdio server: stdout carries the protocol, and\n * the server already writes its banner here.\n */\nconst progress = (message: string): void => {\n process.stderr.write(`[apple-maps-mcp] ${message}\\n`);\n};\n\nexport class MapsWriteError extends AppleAutomationError {\n override readonly name = \"MapsWriteError\";\n}\n\n/** Injected so tests never launch an application. */\nexport type OpenUrl = (url: string) => void;\n\nexport const defaultOpenUrl: OpenUrl = (url) => {\n // `-g` so the user's foreground app is not stolen. `timeout` because a wedged\n // LaunchServices would otherwise block forever, and this runs inside a tool\n // call somebody is waiting on.\n execFileSync(\"/usr/bin/open\", [\"-g\", url], { timeout: 10_000, stdio: \"ignore\" });\n};\n\nexport type AddFavoriteInput = {\n /** What to search for. A name; a bare coordinate does NOT open a place card. */\n query: string;\n latitude?: number | undefined;\n longitude?: number | undefined;\n /** The label to store. Defaults to `query`. */\n name?: string | undefined;\n};\n\nexport type AddFavoriteResult = {\n rowId: number;\n uuid: string;\n name: string | null;\n latitude: number | null;\n longitude: number | null;\n /** False when an equivalent favourite already existed and was returned as-is. */\n created: boolean;\n /** True when Maps had to be asked to resolve the place, leaving a Recents entry. */\n seeded: boolean;\n};\n\n/** The columns needed to return an existing favourite without re-reading it. */\ntype ExistingFavorite = {\n pk: number;\n hex: string;\n name: string | null;\n lat: number;\n lon: number;\n};\n\ntype Donor = {\n mapPk: number;\n lat: number;\n lon: number;\n name: string | null;\n address: string | null;\n category: string | null;\n muid: string | null;\n storage: Uint8Array;\n};\n\nconst uuidBytes = (): Uint8Array =>\n Uint8Array.from(Buffer.from(randomUUID().replaceAll(\"-\", \"\"), \"hex\"));\n\nconst uuidOf = (bytes: Uint8Array): string => {\n const h = Buffer.from(bytes).toString(\"hex\");\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n};\n\n/**\n * Coordinates are floats and never compare exactly across sources, so identity\n * is \"within about a metre\" rather than equality. 1e-5 degrees is ~1.1 m of\n * latitude — close enough that two rows are the same doorway, far enough apart\n * that neighbouring shops are not merged.\n */\nconst NEAR = 1e-5;\n\n/** Same doorway, within about a metre. */\nconst near = (aLat: number, aLon: number, bLat: number, bLon: number): boolean =>\n Math.abs(aLat - bLat) <= SEED_TOLERANCE && Math.abs(aLon - bLon) <= SEED_TOLERANCE;\n\n/**\n * How far a seeded place may sit from the coordinate the caller gave.\n *\n * Far looser than `NEAR`: the caller's coordinate and Apple's for the same place\n * routinely differ by a building's width, and a search resolves to the entrance\n * rather than the pin. ~250 m accepts that and still rejects a different town.\n */\nconst SEED_TOLERANCE = 2.5e-3;\n\nconst toRad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Metres between two coordinates, for the error message only. */\nconst haversineMetres = (aLat: number, aLon: number, bLat: number, bLon: number): number => {\n const dLat = toRad(bLat - aLat);\n const dLon = toRad(bLon - aLon);\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(aLat)) * Math.cos(toRad(bLat)) * Math.sin(dLon / 2) ** 2;\n return 6_371_000 * 2 * Math.asin(Math.sqrt(h));\n};\n\nexport class MapsWriter {\n readonly #path: string;\n readonly #openUrl: OpenUrl;\n readonly #seedTimeoutMs: number;\n\n constructor(opts: { storePath: string; openUrl?: OpenUrl; seedTimeoutMs?: number }) {\n this.#path = opts.storePath;\n this.#openUrl = opts.openUrl ?? defaultOpenUrl;\n this.#seedTimeoutMs = opts.seedTimeoutMs ?? 30_000;\n }\n\n /**\n * A read-write handle.\n *\n * `busy_timeout` is not optional: Maps and `mapssyncd` hold this store open,\n * and without it a write during ordinary use is refused outright rather than\n * waiting for the lock the way any other SQLite client would.\n */\n #open(): DatabaseSync {\n const db = new DatabaseSync(this.#path);\n db.exec(\"PRAGMA busy_timeout = 5000\");\n return db;\n }\n\n #entity(db: DatabaseSync, name: string): number {\n const row = db.prepare(`SELECT Z_ENT AS e FROM Z_PRIMARYKEY WHERE Z_NAME = ?`).get(name) as\n | { e: number | bigint }\n | undefined;\n if (!row)\n throw new MapsWriteError(`This store has no \"${name}\" entity, so it cannot be written.`);\n return Number(row.e);\n }\n\n /**\n * The next primary key: one past whichever is HIGHER, the counter or the\n * highest row actually present.\n *\n * `Z_PRIMARYKEY.Z_MAX` is the counter Core Data maintains, and trusting it\n * alone is what a first version did. It is not always current — Maps can be\n * mid-write, a crash can leave it behind, and a foreign writer that forgot to\n * bump it (a mistake this very file guards against making) leaves it stale\n * forever. Taking `MAX(Z_PK)` as well costs one indexed lookup and makes a\n * collision impossible rather than unlikely.\n *\n * Found by a test whose stand-in for Maps inserted a row without bumping the\n * counter. The stand-in was unfaithful; the bug it exposed was not.\n */\n #nextPk(db: DatabaseSync, name: string, table: string): number {\n const counter = db.prepare(`SELECT Z_MAX AS m FROM Z_PRIMARYKEY WHERE Z_NAME = ?`).get(name) as\n | { m: number | bigint }\n | undefined;\n const highest = db.prepare(`SELECT MAX(Z_PK) AS m FROM \"${table}\"`).get() as\n | { m: number | bigint | null }\n | undefined;\n return Math.max(Number(counter?.m ?? 0), Number(highest?.m ?? 0)) + 1;\n }\n\n /**\n * An existing favourite for this place.\n *\n * Two ways to match, because a coordinate alone is not enough in either\n * direction. TIGHT coordinate (~1 m) catches the canonical case, where both\n * numbers came from Maps. NAME plus a LOOSE coordinate (~250 m) catches the\n * case that matters to a caller: it passed its own coordinate, which differs\n * from Apple's by a building's width — measured at 6.9e-5 for Sagrada Família.\n *\n * Without the second, every repeated call re-seeded before discovering the\n * duplicate, leaving a Recents entry each time for a favourite it then did not\n * create. Loosening the coordinate ALONE was the wrong fix: two different shops\n * 200 m apart would merge. Requiring the name as well makes the loose radius\n * safe, because it only ever merges rows the caller already calls the same\n * thing.\n */\n #existingFavorite(db: DatabaseSync, lat: number, lon: number, name?: string) {\n const tight = db\n .prepare(\n `SELECT Z_PK AS pk, HEX(ZIDENTIFIER) AS hex, ZMAPITEMNAME AS name,\n ZLATITUDE AS lat, ZLONGITUDE AS lon\n FROM ZFAVORITEITEM\n WHERE ZLATITUDE BETWEEN ? AND ? AND ZLONGITUDE BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(lat - NEAR, lat + NEAR, lon - NEAR, lon + NEAR) as ExistingFavorite | undefined;\n if (tight || !name) return tight;\n\n return db\n .prepare(\n `SELECT Z_PK AS pk, HEX(ZIDENTIFIER) AS hex, ZMAPITEMNAME AS name,\n ZLATITUDE AS lat, ZLONGITUDE AS lon\n FROM ZFAVORITEITEM\n WHERE ZMAPITEMNAME = ?\n AND ZLATITUDE BETWEEN ? AND ? AND ZLONGITUDE BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(\n name,\n lat - SEED_TOLERANCE,\n lat + SEED_TOLERANCE,\n lon - SEED_TOLERANCE,\n lon + SEED_TOLERANCE,\n ) as ExistingFavorite | undefined;\n }\n\n /**\n * A place record already in the store for this coordinate.\n *\n * Checked before seeding, so asking for somewhere the user has already looked\n * at costs no Recents entry and no network round trip. Recents, collection\n * items and other favourites are all valid sources — the record is the same\n * object whichever row points at it.\n */\n #donorNear(db: DatabaseSync, lat: number, lon: number): Donor | null {\n const row = db\n .prepare(\n `SELECT m.\"Z_PK\" AS mapPk, m.\"ZLATITUDE\" AS lat, m.\"ZLONGITUDE\" AS lon,\n m.\"ZMAPITEMSTORAGE\" AS storage,\n COALESCE(f.\"ZMAPITEMNAME\", ci.\"ZMAPITEMNAME\") AS name,\n COALESCE(f.\"ZMAPITEMADDRESS\", ci.\"ZMAPITEMADDRESS\") AS address,\n COALESCE(f.\"ZMAPITEMCATEGORY\", ci.\"ZMAPITEMCATEGORY\") AS category,\n CAST(COALESCE(f.\"ZMUID\", ci.\"ZMUID\") AS TEXT) AS muid\n FROM \"ZMIXINMAPITEM\" m\n LEFT JOIN \"ZFAVORITEITEM\" f ON f.\"ZMAPITEM\" = m.\"Z_PK\"\n LEFT JOIN \"ZCOLLECTIONITEM\" ci ON ci.\"ZMAPITEM\" = m.\"Z_PK\"\n WHERE m.\"ZMAPITEMSTORAGE\" IS NOT NULL\n AND m.\"ZLATITUDE\" BETWEEN ? AND ? AND m.\"ZLONGITUDE\" BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(lat - NEAR, lat + NEAR, lon - NEAR, lon + NEAR) as Donor | undefined;\n return row ?? null;\n }\n\n /** The newest recent carrying a place record, above a watermark. */\n #seededDonor(db: DatabaseSync, sinceHistoryPk: number): Donor | null {\n const row = db\n .prepare(\n `SELECT m.\"Z_PK\" AS mapPk, m.\"ZLATITUDE\" AS lat, m.\"ZLONGITUDE\" AS lon,\n m.\"ZMAPITEMSTORAGE\" AS storage,\n h.\"ZCUSTOMNAME\" AS name, NULL AS address, NULL AS category,\n CAST(h.\"ZMUID\" AS TEXT) AS muid\n FROM \"ZHISTORYITEM\" h\n JOIN \"ZMIXINMAPITEM\" m ON m.\"Z_PK\" = h.\"ZMAPITEM\"\n WHERE h.\"Z_PK\" > ? AND m.\"ZMAPITEMSTORAGE\" IS NOT NULL\n ORDER BY h.\"Z_PK\" DESC LIMIT 1`,\n )\n .get(sinceHistoryPk) as Donor | undefined;\n return row ?? null;\n }\n\n #maxHistory(db: DatabaseSync): number {\n const row = db.prepare(`SELECT MAX(Z_PK) AS m FROM ZHISTORYITEM`).get() as\n | { m: number | bigint | null }\n | undefined;\n return Number(row?.m ?? 0);\n }\n\n /**\n * Add a favourite, in three phases with a connection open for as little of it\n * as possible.\n *\n * THE PHASES ARE THE POINT. A first version opened one read-write handle at the\n * top and held it across the whole seed — including the wait for Maps to\n * resolve a place, which can run to tens of seconds. That hung: Maps is being\n * asked to WRITE the very record being waited for, into a store this process is\n * holding open for writing. Whatever the precise interaction, the shape was a\n * departure from the sequence that had been proven by hand, and the proven\n * sequence never holds a write handle while waiting on the app.\n *\n * 1. READ — look for an existing favourite and an existing place record.\n * 2. SEED — ask Maps to mint a record, holding NO connection at all, and\n * poll with a short-lived read-only handle each time.\n * 3. WRITE — open read-write, insert, close.\n */\n addFavorite(input: AddFavoriteInput): AddFavoriteResult {\n const hasCoords = input.latitude !== undefined && input.longitude !== undefined;\n const lat = input.latitude ?? 0;\n const lon = input.longitude ?? 0;\n\n // ── 1. read ──────────────────────────────────────────────────────────────\n progress(`add_favorite ${JSON.stringify(input.query)}: reading the store`);\n const ro = new DatabaseSync(this.#path, { readOnly: true });\n let existing: {\n pk: number;\n hex: string;\n name: string | null;\n lat: number;\n lon: number;\n } | null = null;\n let donor: Donor | null = null;\n let watermark = 0;\n try {\n if (hasCoords) {\n existing = this.#existingFavorite(ro, lat, lon, input.name ?? input.query) ?? null;\n donor = this.#donorNear(ro, lat, lon);\n }\n watermark = this.#maxHistory(ro);\n } finally {\n ro.close();\n }\n\n if (existing) {\n // Idempotent on purpose. A model that retries must not produce two\n // favourites for one place — and Maps HIDES the duplicate in its own UI,\n // so the store would disagree with the app invisibly.\n return {\n rowId: Number(existing.pk),\n uuid: uuidOf(Buffer.from(existing.hex, \"hex\")),\n name: existing.name,\n latitude: Number(existing.lat),\n longitude: Number(existing.lon),\n created: false,\n seeded: false,\n };\n }\n\n // ── 2. seed, holding nothing ─────────────────────────────────────────────\n let seeded = false;\n if (!donor) {\n /*\n * THE SEED URL CARRIES NO COORDINATE, and that is measured rather than\n * stylistic.\n *\n * `maps://?q=<name>` is the form proved to make Maps file the place in\n * Recents with a record attached. An earlier version appended\n * `&ll=<lat>,<lon>` on the strength of an Accessibility measurement where\n * that form opened a place CARD — but a card is not a Recents entry, and\n * with the coordinate present Maps appears to treat the URL as \"show this\n * location\" rather than a search worth recording. Nothing was ever written,\n * so the poll ran to its timeout every time.\n *\n * The coordinate is still used, for de-duplication and for checking that\n * what Maps resolved is the place that was asked for. It just does not go\n * into the URL.\n */\n const url = `maps://?q=${encodeURIComponent(input.query)}`;\n progress(`asking Maps to resolve ${JSON.stringify(input.query)}…`);\n this.#openUrl(url);\n seeded = true;\n progress(`opened, waiting up to ${Math.round(this.#seedTimeoutMs / 1000)}s for a record`);\n const deadline = Date.now() + this.#seedTimeoutMs;\n let polls = 0;\n while (Date.now() < deadline && !donor) {\n // A fresh handle per poll: under WAL a connection that has begun reading\n // holds a snapshot and may not see another process's commits.\n const probe = new DatabaseSync(this.#path, { readOnly: true });\n try {\n donor = this.#seededDonor(probe, watermark);\n } finally {\n probe.close();\n }\n if (!donor) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, STORAGE_POLL_MS);\n polls += 1;\n if (polls % 8 === 0) progress(`still waiting (${polls * (STORAGE_POLL_MS / 1000)}s)…`);\n }\n if (donor && hasCoords && !near(donor.lat, donor.lon, lat, lon)) {\n // Maps resolved SOMETHING, but not the place that was asked for. Copying\n // its record would make a favourite for the wrong place — which then\n // syncs to every device on the account — so this refuses rather than\n // guesses.\n throw new MapsWriteError(\n `Maps resolved ${JSON.stringify(input.query)} to a place about ` +\n `${Math.round(haversineMetres(donor.lat, donor.lon, lat, lon))}m from the coordinate ` +\n `given, so it is probably not the place meant. Nothing was saved — try a more ` +\n `specific name.`,\n { query: input.query },\n );\n }\n if (donor)\n progress(`got a place record (${donor.storage?.length ?? 0} bytes) after ${polls} polls`);\n if (!donor) {\n throw new MapsWriteError(\n `Maps did not produce a place record for \"${input.query}\" within ` +\n `${Math.round(this.#seedTimeoutMs / 1000)}s. Maps must be RUNNING for this to work — ` +\n `it is the only thing that can create a place record. It may also have resolved the ` +\n `query to a search rather than a place, since searches get no record. Try a more ` +\n `specific name, or pass latitude and longitude.`,\n { query: input.query },\n );\n }\n }\n\n /*\n * DE-DUPLICATE AGAIN, against the RESOLVED coordinate.\n *\n * The check in phase 1 uses the coordinate the CALLER supplied, and that is\n * the wrong kind of number to compare with. MEASURED: asked to save Sagrada\n * Família at longitude 2.1744, Maps resolved and stored 2.1743308 — 6.9e-5\n * away, seven times the ~1 m tolerance. So a caller repeating its own request\n * verbatim did not match the row its first call had created, and a second\n * favourite appeared.\n *\n * Maps' own coordinate is canonical: whatever it resolved this time, it\n * resolved last time too. Comparing that against what is stored is comparing\n * like with like, and it is exact enough for the tight tolerance.\n *\n * The phase-1 check still earns its place — when it hits, no seeding happens\n * and no Recents entry appears. This one is what makes the tool correct.\n */\n const canonical = new DatabaseSync(this.#path, { readOnly: true });\n let duplicate: ExistingFavorite | null = null;\n try {\n duplicate = this.#existingFavorite(canonical, donor.lat, donor.lon) ?? null;\n } finally {\n canonical.close();\n }\n if (duplicate) {\n progress(\"already a favourite; nothing written\");\n return {\n rowId: Number(duplicate.pk),\n uuid: uuidOf(Buffer.from(duplicate.hex, \"hex\")),\n name: duplicate.name,\n latitude: Number(duplicate.lat),\n longitude: Number(duplicate.lon),\n created: false,\n seeded,\n };\n }\n\n // ── 3. write ─────────────────────────────────────────────────────────────\n progress(\"writing the favourite\");\n const db = this.#open();\n try {\n const favEnt = this.#entity(db, \"FavoriteItem\");\n const mixEnt = this.#entity(db, \"MixinMapItem\");\n const favPk = this.#nextPk(db, \"FavoriteItem\", \"ZFAVORITEITEM\");\n const mixPk = this.#nextPk(db, \"MixinMapItem\", \"ZMIXINMAPITEM\");\n const now = Date.now() / 1000 - CORE_DATA_EPOCH_OFFSET;\n const id = uuidBytes();\n const label = input.name ?? donor.name ?? input.query;\n const position = Number(\n (db.prepare(`SELECT COUNT(*) AS c FROM ZFAVORITEITEM`).get() as { c: number | bigint }).c,\n );\n\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n db.prepare(\n `INSERT INTO \"ZFAVORITEITEM\"\n (Z_PK, Z_ENT, Z_OPT, ZHIDDEN, ZPOSITIONINDEX, ZSOURCE, ZTYPE, ZVERSION,\n ZMAPITEM, ZMUID, ZCREATETIME, ZMODIFICATIONTIME, ZMAPITEMLASTREFRESHED,\n ZLATITUDE, ZLONGITUDE, ZMAPITEMNAME, ZMAPITEMADDRESS, ZMAPITEMCATEGORY,\n ZIDENTIFIER)\n VALUES (?, ?, 1, 0, ?, 0, 1, 2, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n favPk,\n favEnt,\n position,\n mixPk,\n donor.muid === null ? null : Number(donor.muid),\n now,\n now,\n now,\n donor.lat,\n donor.lon,\n label,\n donor.address,\n donor.category,\n id,\n );\n db.prepare(\n `INSERT INTO \"ZMIXINMAPITEM\"\n (Z_PK, Z_ENT, Z_OPT, ZFAVORITEITEM, ZCREATETIME, ZMODIFICATIONTIME,\n ZLATITUDE, ZLONGITUDE, ZMAPITEMSTORAGE)\n VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?)`,\n ).run(mixPk, mixEnt, favPk, now, now, donor.lat, donor.lon, donor.storage);\n // Not bumping these hands the next insert BY MAPS a duplicate primary\n // key, which is a corruption of the app's data by our omission.\n db.prepare(`UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_NAME = 'FavoriteItem'`).run(favPk);\n db.prepare(`UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_NAME = 'MixinMapItem'`).run(mixPk);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // Already resolved; the throw below is the report.\n }\n throw err;\n }\n\n return {\n rowId: favPk,\n uuid: uuidOf(id),\n name: label,\n latitude: Number(donor.lat),\n longitude: Number(donor.lon),\n created: true,\n seeded,\n };\n } finally {\n db.close();\n }\n }\n\n /**\n * Remove a favourite and the place record it owns.\n *\n * `Z_MAX` is deliberately NOT rolled back. Core Data never reuses a primary\n * key, and decrementing the counter would hand the next insert one that is\n * already spoken for by a row still referenced elsewhere.\n */\n removeFavorite(key: EntityKey): boolean {\n const db = this.#open();\n try {\n const where = \"uuid\" in key ? `HEX(ZIDENTIFIER) = ?` : `Z_PK = ?`;\n const param = \"uuid\" in key ? key.uuid.replaceAll(\"-\", \"\").toUpperCase() : key.rowId;\n const row = db\n .prepare(`SELECT Z_PK AS pk, ZMAPITEM AS mapPk FROM ZFAVORITEITEM WHERE ${where} LIMIT 1`)\n .get(param) as { pk: number; mapPk: number | null } | undefined;\n if (!row) return false;\n\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n if (row.mapPk !== null) {\n db.prepare(`DELETE FROM ZMIXINMAPITEM WHERE Z_PK = ?`).run(row.mapPk);\n }\n db.prepare(`DELETE FROM ZFAVORITEITEM WHERE Z_PK = ?`).run(row.pk);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // Already resolved.\n }\n throw err;\n }\n return true;\n } finally {\n db.close();\n }\n }\n}\n","import type { Logger, ReadOnlyMode } from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport { renderInstant, type Epoch } from \"./dates.js\";\nimport { MapsStoreUnavailableError } from \"./errors.js\";\nimport { locateStore, type LocateResult } from \"./locate.js\";\nimport { encodeCollectionRef, encodePlaceRef, type PlaceKind } from \"./ref.js\";\nimport type { EntityKey } from \"./store.js\";\nimport { openStore, type CollectionRow, type MapsStore, type PlaceRow } from \"./store.js\";\nimport { MapsWriter, type OpenUrl } from \"./write.js\";\n\n/**\n * The client.\n *\n * ## One lane, and the consequence of that\n *\n * Safari's orchestrator exists to keep two lanes from being mistaken for one\n * another. Maps has no second lane at all — the app ships no scripting\n * dictionary — so this class has the opposite job: making sure the ABSENCE of\n * the one lane is never mistaken for an absence of data.\n *\n * Without Full Disk Access every list would naturally come back `[]`, and an\n * empty `favorites` reads exactly like a person who has saved no places. So a\n * read with no store THROWS a named error; it never returns an empty array.\n * That is the same rule `packages/messages` follows, and for the same reason:\n * both are surfaces where the grant is not an optimisation.\n */\n\nexport type RenderedPlace = {\n ref: string;\n kind: PlaceKind;\n /** The user's own label when they set one, otherwise the place's name. */\n name: string | null;\n /** The place's own name, kept separate so a rename is visible as one. */\n placeName: string | null;\n address: string | null;\n latitude: number | null;\n longitude: number | null;\n /**\n * Apple's place identifier. Stable for the same place across devices, and\n * shared between a favourite and a collection entry for the same place — so\n * it is reported, and never used as the ref. See `ref.ts`.\n *\n * A STRING: it is a 64-bit integer that does not fit in a JS number. A real\n * store returned `-2679868148951248105`.\n */\n muid: string | null;\n created: string | null;\n modified: string | null;\n /**\n * False for a row with no linked place record. MEASURED: 3 of 23 favourites,\n * carrying no name and no coordinate — almost certainly the unconfigured\n * Home / Work / School slots Maps creates whether or not anyone fills them in.\n */\n linked: boolean;\n};\n\nexport type RenderedCollection = {\n ref: string;\n title: string | null;\n /** Maps' own count, which may include items this server cannot enumerate. */\n placesCount: number | null;\n created: string | null;\n modified: string | null;\n};\n\nexport type CreateClientOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so discovery never reaches the developer's real home. */\n home?: string;\n};\n\n/**\n * What diagnostics says about one entity: how many rows, which column won for\n * each field, and which fields resolved to nothing. `resolved` is the part that\n * matters when a result looks wrong — it names the column actually being read.\n */\nconst summariseEntity = (e: MapsStore[\"caps\"][\"favorites\"]) => ({\n rows: e.rows,\n resolved: Object.fromEntries(Object.entries(e.fields).filter(([, v]) => v !== null)),\n unresolved: Object.entries(e.fields)\n .filter(([, v]) => v === null)\n .map(([k]) => k),\n});\n\nexport class AppleMapsClient {\n readonly #config: Config;\n readonly #logger: Logger | undefined;\n readonly #home: string | undefined;\n\n #located: LocateResult | null = null;\n #store: MapsStore | null = null;\n #storeError: string | null = null;\n\n constructor(opts: CreateClientOptions) {\n this.#config = opts.config;\n this.#logger = opts.logger;\n this.#home = opts.home;\n }\n\n get config(): Config {\n return this.#config;\n }\n\n located(): LocateResult {\n this.#located ??= locateStore({\n storePath: this.#config.storePath,\n ...(this.#home ? { home: this.#home } : {}),\n });\n return this.#located;\n }\n\n /**\n * A writer bound to the located store, or an error explaining why not.\n *\n * Writes go through their own read-write connection rather than the shared\n * read-only one: `store()` opens with `query_only` and an `immutable` fallback\n * precisely so a read can never mutate by accident, and widening it would\n * throw that guarantee away for every read on the surface.\n */\n writer(openUrl?: OpenUrl): MapsWriter {\n const located = this.located();\n if (!located.readable || !located.storePath) {\n throw new MapsStoreUnavailableError(located.reason ?? \"Maps' store could not be opened.\");\n }\n return new MapsWriter({\n storePath: located.storePath,\n ...(openUrl ? { openUrl } : {}),\n });\n }\n\n /** Open the store, once, lazily. Every read goes through here. */\n store(): MapsStore {\n if (this.#store) return this.#store;\n if (this.#storeError !== null) throw new MapsStoreUnavailableError(this.#storeError);\n\n const located = this.located();\n if (!located.readable || !located.storePath) {\n this.#storeError = located.reason ?? \"Maps' store could not be opened.\";\n throw new MapsStoreUnavailableError(this.#storeError);\n }\n try {\n this.#store = openStore({\n path: located.storePath,\n mode: this.#config.indexMode as ReadOnlyMode,\n ...(this.#logger ? { logger: this.#logger } : {}),\n });\n return this.#store;\n } catch (err) {\n this.#storeError = err instanceof Error ? err.message : String(err);\n throw err;\n }\n }\n\n get epoch(): Epoch {\n return this.store().caps.epoch;\n }\n\n #render(kind: PlaceKind, row: PlaceRow, epoch: Epoch): RenderedPlace {\n return {\n ref: encodePlaceRef(kind, row.uuid ? { uuid: row.uuid } : { rowId: row.id }),\n kind,\n name: row.customName ?? row.name,\n placeName: row.name,\n address: row.address,\n latitude: row.latitude,\n longitude: row.longitude,\n muid: row.muid,\n created: renderInstant(row.createdRaw, epoch),\n modified: renderInstant(row.modifiedRaw, epoch),\n linked: row.linked,\n };\n }\n\n #renderCollection(row: CollectionRow, epoch: Epoch): RenderedCollection {\n return {\n ref: encodeCollectionRef(row.uuid ? { uuid: row.uuid } : { rowId: row.id }),\n title: row.title,\n placesCount: row.placesCount,\n created: renderInstant(row.createdRaw, epoch),\n modified: renderInstant(row.modifiedRaw, epoch),\n };\n }\n\n places(\n kind: PlaceKind,\n opts: {\n limit: number;\n collectionId?: number | undefined;\n query?: string | undefined;\n unfiled?: boolean | undefined;\n },\n ): { places: RenderedPlace[]; truncated: boolean; datesAvailable: boolean } {\n const store = this.store();\n const { rows, truncated } = store.places(kind, opts);\n return {\n places: rows.map((r) => this.#render(kind, r, store.caps.epoch)),\n truncated,\n datesAvailable: store.caps.epoch.confident,\n };\n }\n\n place(kind: PlaceKind, key: EntityKey): RenderedPlace | null {\n const store = this.store();\n const row = store.place(kind, key);\n return row ? this.#render(kind, row, store.caps.epoch) : null;\n }\n\n /**\n * A collection ref to the row id its items point at, or null when the ref\n * addresses nothing in this store.\n *\n * Collection membership is a Core Data foreign key and always holds `Z_PK`,\n * so a uuid ref has to be translated before it can filter items.\n */\n collectionRowId(key: EntityKey): number | null {\n return this.store().collectionRowId(key);\n }\n\n collections(opts: { limit: number }): {\n collections: RenderedCollection[];\n truncated: boolean;\n /** Null when the membership key was not found — see `store.ts`. */\n itemsEnumerable: boolean;\n /**\n * Saved places filed in no collection, or null when unanswerable.\n *\n * Reported beside the collections because the counts otherwise do not add\n * up and nothing says so: the guides account for 18 places while the store\n * holds 30, and a caller comparing the two has no way to learn where the\n * rest went.\n */\n unfiled: number | null;\n } {\n const store = this.store();\n const { rows, truncated } = store.collections(opts);\n return {\n collections: rows.map((r) => this.#renderCollection(r, store.caps.epoch)),\n truncated,\n itemsEnumerable: store.caps.membership !== null,\n unfiled: store.unfiledCount(),\n };\n }\n\n /**\n * Search every place-bearing entity at once.\n *\n * Three separate queries rather than a UNION, because the three tables have\n * different resolved columns and a UNION would have to flatten them to the\n * narrowest — which on this store means losing history's coordinates, since\n * they live in a differently named column from the other two.\n */\n search(opts: { query: string; limit: number }): {\n places: RenderedPlace[];\n truncated: boolean;\n datesAvailable: boolean;\n } {\n const store = this.store();\n const kinds: PlaceKind[] = [\"favorite\", \"collection-item\", \"history\"];\n const places: RenderedPlace[] = [];\n let truncated = false;\n for (const kind of kinds) {\n const { rows, truncated: t } = store.places(kind, {\n limit: opts.limit,\n query: opts.query,\n });\n truncated ||= t;\n for (const r of rows) places.push(this.#render(kind, r, store.caps.epoch));\n }\n return {\n places: places.slice(0, opts.limit),\n truncated: truncated || places.length > opts.limit,\n datesAvailable: store.caps.epoch.confident,\n };\n }\n\n /** Everything diagnostics needs, with no failure allowed to fail the call. */\n status(): {\n located: LocateResult;\n store: { opened: boolean; mode: string | null; reason: string | null };\n capabilities: Record<string, unknown> | null;\n } {\n const located = this.located();\n let store: MapsStore | null = null;\n let reason: string | null = located.reason;\n try {\n store = this.store();\n } catch (err) {\n reason = err instanceof Error ? err.message : String(err);\n }\n return {\n located,\n store: { opened: store !== null, mode: store?.mode ?? null, reason },\n capabilities: store\n ? {\n fingerprint: store.caps.fingerprint,\n tableCount: store.caps.tables.length,\n epoch: store.caps.epoch,\n collectionFk: store.caps.collectionFk,\n collectionMembership: store.caps.membership,\n entities: {\n favorites: summariseEntity(store.caps.favorites),\n collections: summariseEntity(store.caps.collections),\n collectionItems: summariseEntity(store.caps.collectionItems),\n history: summariseEntity(store.caps.history),\n mapItems: summariseEntity(store.caps.mapItems),\n },\n }\n : null,\n };\n }\n\n close(): void {\n this.#store?.close();\n this.#store = null;\n }\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, which is the whole point.\n *\n * `allowWrites` gates two mutating tools, and gates them harder than elsewhere.\n * Maps has no scripting dictionary and no registered App Intents, so a write is\n * SQL straight into a Core Data store that `NSPersistentCloudKitContainer`\n * mirrors — which means it reaches every device on the account, not just this\n * Mac. That was measured, along with the rule that keeps it safe: never\n * fabricate a place record, only ever copy one Maps wrote itself. See\n * `docs/maps.md` and `client/write.ts`.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /** Explicit store path. Bypasses discovery — for tests and forensic copies. */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\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_MAPS_ALLOW_WRITES),\n exposePrompts: parseBool(env.APPLE_MAPS_EXPOSE_PROMPTS),\n debug: parseBool(env.APPLE_MAPS_DEBUG),\n storePath: trimmed(env.APPLE_MAPS_STORE),\n indexMode: trimmed(env.APPLE_MAPS_INDEX_MODE),\n osascriptPath: trimmed(env.APPLE_MAPS_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_MAPS_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_MAPS_MAX_RESULTS),\n });\n","/**\n * The Maps operating manual, served as `cupertino://maps/guide` and embedded\n * ahead of every Maps prompt. Static by design — see the note in the Mail guide.\n */\nexport const MAPS_GUIDE = `# Maps — how to drive this server\n\n## What this surface is, and is not\n\nIt reads the places **saved on this Mac**: favourites, collections (Guides) and\nrecents. It does **not** search Apple's map of the world, geocode an address,\ngive directions, or compute a travel time. If asked for any of those, say so\nplainly rather than searching the saved places and presenting a near-miss —\n\"the nearest coffee shop\" is not a question this server can answer, and a\nfavourite called \"Coffee\" is not the answer to it.\n\n## One lane, and no fallback\n\nMaps ships **no scripting dictionary** — there is no \\`.sdef\\` in the app bundle\n— so unlike every other surface here there is no Apple Events lane to fall back\nto. Everything comes from a Core Data store under **Full Disk Access**.\n\nThe consequence matters: without the grant this server returns an **error**, not\nan empty list. If a tool ever does return an empty list, that means the user has\ngenuinely saved nothing of that kind. Check\n\\`cupertino://maps/diagnostics\\` before concluding either way.\n\n## Which tool answers which question\n\n| Question | Tool |\n| --- | --- |\n| What places has the user saved? | \\`apple_maps_list_favorites\\` |\n| What Guides do they keep? | \\`apple_maps_list_collections\\` |\n| What is filed in one Guide? | \\`apple_maps_list_collection_places\\` |\n| Where have they looked recently? | \\`apple_maps_list_recents\\` |\n| Find a saved place by name or address | \\`apple_maps_search_places\\` |\n| One place, in full | \\`apple_maps_get_place\\` |\n\n## Four things not to say\n\n**\\`linked: false\\` is not a broken row.** Some favourites have no place\nattached, no name and no coordinate. They are Maps' unconfigured Home / Work /\nSchool slots. Report them as unset, not as places.\n\n**A null date is unknown, not old.** Timestamps are placed on an epoch detected\nfrom the store. When detection fails every date reads \\`null\\` rather than being\nguessed, because a date wrong by 31 years reads exactly like a correct one.\n\n**A collection with no listable places may still have places.** How an item\nbelongs to a collection is not exposed by every version of this store. When the\nkey is missing, \\`placesCount\\` is still Maps' own accurate number and the\nplaces simply cannot be enumerated. Say that, rather than reporting an empty\nGuide.\n\n**Refs expire.** They address a row in a store that iCloud re-syncs, and Core\nData reuses row ids after a delete. A ref is good for this conversation. Do not\nstore one or hand it back later.\n\n## Privacy, worth holding in mind\n\nSaved places are a home address, a doctor, a school, a partner's flat. This is\namong the most sensitive data any surface here reads. Answer what was asked;\ndo not enumerate everything because a listing tool is available.\n`;\n","import { promptArg, registerWorkflowPrompt, type PromptContext } from \"@mgcrea/mcp-apple-core\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { MAPS_GUIDE } from \"./guide.js\";\n\nconst CTX: PromptContext = { surface: \"maps\", guide: MAPS_GUIDE };\n\n/**\n * Maps' one workflow prompt.\n *\n * It exists because \"where was that place\" spans three lists that hold the same\n * kind of thing for different reasons — a favourite is deliberate, a collection\n * entry is filed, a recent is incidental — and searching only one produces a\n * confident answer to a question nobody asked. The prompt also carries the\n * refusal: this server does not know about places the user has never saved, and\n * the most likely way to be wrong here is to answer anyway.\n */\nexport const registerPrompts = (server: McpServer): void => {\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_maps_where_was_that_place\",\n title: \"Find a place I saved\",\n description:\n \"Track down a saved place across favourites, Guides and recents — three lists that hold \" +\n \"places for different reasons. Read-only.\",\n argsSchema: {\n about: promptArg('What the place was, e.g. \"the ramen place near the office\".'),\n },\n build: ({ about }) => `Find the saved place ${about ? `matching: ${about}` : \"I am after\"}.\n\nStart with \\`apple_maps_search_places\\` — it covers favourites, Guide entries and\nrecents in one call, and tells you which kind each match came from. That\ndistinction is the answer, not a detail: a favourite is somewhere I chose to\nkeep, a recent is somewhere I merely looked at once.\n\nIf that finds nothing, list the three directly before concluding it is not\nthere — a place I saved under my own label (\"Mum's\") will not match a search for\nwhat it actually is.\n\nThen report what you found, with the address and how it was saved.\n\nThree things to get right:\n- If nothing matches, say I have not saved it. Do **not** fall back to guessing\n a place from general knowledge — this server only knows what is on this Mac,\n and inventing an address is the worst possible failure here.\n- An entry with \\`linked: false\\` is an unconfigured Home/Work/School slot, not\n a place. Do not report it as one.\n- If a date reads null, the date is unknown. Do not describe it as old.\n\nIf every list comes back empty, check \\`cupertino://maps/diagnostics\\` before\nconcluding I have saved nothing — this surface has no second lane, so a missing\nFull Disk Access grant takes all of it at once.`,\n });\n};\n","/**\n * Maps' tool helpers. The generic half lives in `@mgcrea/mcp-apple-core`;\n * re-exported here so tools import from one place.\n */\nexport {\n compact,\n fail,\n limitArg,\n ok,\n okText,\n toFailure,\n wrap,\n wrapResult,\n type ToolResult,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { z } from \"zod\";\n\nexport const placeRefArg = z\n .string()\n .min(1)\n .describe(\n 'An opaque place ref from a listing (looks like \"p1:f:12\"). Do not construct one by hand. ' +\n \"Refs address a row in the local store, so they are only valid for this session — an \" +\n \"iCloud re-sync can renumber them.\",\n );\n\nexport const collectionRefArg = z\n .string()\n .min(1)\n .describe('An opaque collection ref from apple_maps_list_collections (looks like \"pc1:3\").');\n\nexport const queryArg = z\n .string()\n .min(1)\n .describe(\n \"Text to look for in a place's name, the label you gave it, or its address. Wildcards are \" +\n 'escaped, so searching for \"100%\" finds that literal string.',\n );\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"../build-info.js\";\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * Build the report.\n *\n * Split out of the tool registration so the `cupertino://maps/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: AppleMapsClient,\n): Promise<Record<string, unknown>> => {\n const status = client.status();\n const located = status.located;\n return {\n server: { name: BUILD_INFO.name, version: BUILD_INFO.version },\n settings: { exposePrompts: client.config.exposePrompts },\n lanes: {\n summary:\n \"Maps has ONE lane. Maps.app ships no scripting dictionary — there is no .sdef in the \" +\n \"bundle, checked directly — so there is no Apple Events fallback. Without Full Disk \" +\n \"Access this server cannot read anything at all, and it says so rather than returning \" +\n \"empty lists.\",\n fileLane: {\n needs: \"Full Disk Access\",\n answers: \"favourites, collections (guides), recents\",\n working: status.store.opened,\n },\n appleEvents: \"none — Maps is not scriptable\",\n writes:\n \"none — this server registers no mutating tool. The store is mirrored to iCloud by \" +\n \"NSPersistentCloudKitContainer, so a write is an edit to one replica of a \" +\n \"synchronising graph underneath a running app. That was never probed.\",\n },\n store: {\n path: located.storePath,\n exists: located.exists,\n readable: located.readable,\n opened: status.store.opened,\n mode: status.store.mode,\n reason: status.store.reason,\n resolvedByScan: located.resolvedByScan,\n ...status.capabilities,\n },\n files: {\n directory: located.directory,\n localCache: {\n path: located.localCache.path,\n exists: located.localCache.exists,\n note:\n \"The device-local cache. Same entities as the sync store and, on the probed \" +\n \"machine, zero rows in every one of them. Located so it is visibly considered, \" +\n \"never opened.\",\n },\n },\n caveats: [\n \"This store was missed three times before it was found, and the reasons are worth \" +\n \"knowing if it ever looks absent: the file has NO EXTENSION (MapsSync_0.0.1), it \" +\n \"lives in the one directory of Maps' container that Full Disk Access gates, and \" +\n \"`group.com.apple.Maps` is a decoy that is EPERM rather than empty.\",\n \"Columns are resolved BY COVERAGE, not by name. ZHISTORYITEM carries both ZLATITUDE \" +\n \"(1 row of 33) and ZLATITUDE1 (19 of 33); picking the first recognised name would \" +\n \"report that Maps holds almost no coordinates. `entities.*.resolved` above shows \" +\n \"which column actually won for each field.\",\n \"Some favourites have no linked place: 3 of 23 on the probed machine, with no name \" +\n \"and no coordinate. Almost certainly the unconfigured Home / Work / School slots. \" +\n \"They are returned with `linked: false` rather than dropped, because silently \" +\n \"omitting rows reads as a deletion.\",\n \"Collection membership is a many-to-many join table, Z_6PLACES(Z_6COLLECTIONS, \" +\n \"Z_7PLACES) on the probed machine, NOT a column on ZCOLLECTIONITEM — which is why \" +\n \"four guessed column names all missed it. It is re-proved at open time by \" +\n \"reproducing ZPLACESCOUNT exactly, never by matching a name, because \" +\n \"ZCOLLECTIONITEM.ZMAPITEM joins 3 of 10 collections by coincidence. When nothing \" +\n \"reproduces those counts, `collectionMembership` is null, collections list \" +\n \"without their places and every result says so.\",\n \"Refs address a local row id. Core Data reuses those after a delete and this store is \" +\n \"mirrored from CloudKit, so a re-sync can renumber rows. A ref is good for the \" +\n \"current session and should not be stored.\",\n \"Timestamps are placed on an epoch DETECTED from the store, never assumed. The same \" +\n \"value read as unix seconds instead of Core Data seconds lands in 1995 and looks \" +\n \"entirely plausible. When detection fails every date reads null rather than guessed.\",\n ],\n };\n};\n\nexport const registerDiagnosticsTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_diagnostics\",\n {\n description:\n \"Report whether Maps' store can be read, which columns were resolved for each entity, \" +\n \"and what this server deliberately cannot do. Start here when a read returns nothing — \" +\n \"this surface has no second lane, so a missing grant means no data at all rather than \" +\n \"slower data.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => buildDiagnostics(client)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { decodeCollectionRef, decodePlaceRef } from \"../client/ref.js\";\nimport {\n collectionRefArg,\n compact,\n fail,\n limitArg,\n ok,\n placeRefArg,\n queryArg,\n wrapResult,\n} from \"./util.js\";\n\n/**\n * The place tools.\n *\n * Every description states the permission fact plainly. On this surface it is\n * not the usual \"slower without the grant\" — there is no second lane, so\n * without Full Disk Access these tools have nothing to read. Saying so in the\n * description is what stops a model concluding, from an error it half-read,\n * that the person simply has not saved any places.\n */\nexport const registerPlaceTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_list_favorites\",\n {\n description:\n \"List the places saved as favourites in Maps, with coordinates, address and the label \" +\n \"the user gave them. Needs Full Disk Access — Maps is not scriptable, so without the \" +\n \"grant this returns an error rather than an empty list. Some entries have no linked \" +\n \"place (`linked: false`); those are the unconfigured Home/Work/School slots, not \" +\n \"broken rows. This is everything SAVED: Maps' own Pinned panel applies display rules \" +\n \"that are not in the store and can show fewer.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.places(\"favorite\", { limit: limit ?? client.config.maxResults });\n const unlinked = result.places.filter((p) => !p.linked).length;\n return ok(\n compact({\n favorites: result.places,\n count: result.places.length,\n unlinked: unlinked\n ? `${unlinked} entr${unlinked === 1 ? \"y has\" : \"ies have\"} no linked place — ` +\n `these are Maps' unconfigured Home/Work/School slots. They are returned rather ` +\n `than dropped, because silently omitting rows reads as a deletion.`\n : undefined,\n /**\n * MEASURED, and the reason the note above no longer claims this count\n * matches the app: on a real store Maps' Pinned panel showed 17 while\n * this tool returned 24. Three extras were the unlinked slots; four\n * were ordinary favourites with names, addresses and coordinates, one\n * an exact duplicate of a shown entry.\n *\n * No column in ZFAVORITEITEM separates them — ZHIDDEN, ZSOURCE, ZTYPE\n * and ZVERSION were all cross-tabulated and none yields a group of\n * four. `ZVERSION = 2` has exactly 17 rows and is a COINCIDENCE OF\n * TOTALS: 16 of them are linked while all 17 shown entries are. So\n * Maps applies display rules this server cannot see — de-duplication\n * is the visible one — and no filter written here could reproduce\n * them. Saying so beats guessing, and beats the old note, which\n * asserted the opposite of what was measured. See docs/maps.md.\n */\n mayExceedApp:\n \"Maps' own Pinned panel can show FEWER places than this. It applies display \" +\n \"rules — de-duplication at least — that are not recorded in the store, so this \" +\n \"is everything saved, not everything shown.\",\n truncated: result.truncated\n ? `More favourites exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n datesUnavailable: result.datesAvailable\n ? undefined\n : \"Timestamps could not be placed on a known epoch, so every date reads null. \" +\n \"They are withheld rather than guessed — see apple_maps_diagnostics.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_collections\",\n {\n description:\n \"List the collections (Guides) in Maps. Each carries Maps' own count of the places in \" +\n \"it. Use apple_maps_list_collection_places to enumerate one. Needs Full Disk Access.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.collections({ limit: limit ?? client.config.maxResults });\n return ok(\n compact({\n collections: result.collections,\n count: result.collections.length,\n // The guides do not account for every saved place, and silence\n // about that reads as \"there are no others\".\n unfiled:\n result.unfiled && result.unfiled > 0\n ? `${result.unfiled} saved place(s) are in no collection. ` +\n `List them with apple_maps_list_unfiled_places.`\n : undefined,\n // The honest version of a capability this server may not have.\n itemsUnavailable: result.itemsEnumerable\n ? undefined\n : \"This store does not expose how an item belongs to a collection, so the places \" +\n \"inside a collection cannot be listed. `placesCount` is Maps' own number and is \" +\n \"still accurate; apple_maps_list_collection_places will return nothing.\",\n truncated: result.truncated\n ? `More collections exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_collection_places\",\n {\n description:\n \"List the places filed in one collection, by its ref. Needs Full Disk Access. Returns \" +\n \"nothing when this store does not expose collection membership — check \" +\n \"apple_maps_diagnostics, which reports whether the key was resolved.\",\n inputSchema: { ref: collectionRefArg, limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ ref, limit }) =>\n wrapResult(async () => {\n const collectionId = client.collectionRowId(decodeCollectionRef(ref));\n const result = client.places(\"collection-item\", {\n limit: limit ?? client.config.maxResults,\n collectionId: collectionId ?? undefined,\n });\n const collections = client.collections({ limit: 1_000 });\n if (!collections.itemsEnumerable) {\n return fail(\n \"This store does not expose which collection an item belongs to, so its places \" +\n \"cannot be listed. The collection itself and its place count are still readable \" +\n \"through apple_maps_list_collections.\",\n );\n }\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More places exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_unfiled_places\",\n {\n description:\n \"List saved places that are in no collection. Maps files a saved place into a Guide \" +\n \"through a join table, and a place can exist with no row there — 12 of 30 on the probed \" +\n \"machine, 7 of which appear nowhere else in the store: not as a favourite, not in \" +\n \"another Guide, not in recents. Those are reachable through no other tool. Needs Full \" +\n \"Disk Access. Returns nothing when collection membership could not be resolved, which \" +\n \"apple_maps_diagnostics reports.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const collections = client.collections({ limit: 1 });\n if (!collections.itemsEnumerable) {\n return fail(\n \"This store does not expose which collection an item belongs to, so places in no \" +\n \"collection cannot be told apart from places in one. \" +\n \"apple_maps_list_collections still works.\",\n );\n }\n const result = client.places(\"collection-item\", {\n limit: limit ?? client.config.maxResults,\n unfiled: true,\n });\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More places exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_recents\",\n {\n description:\n \"List the places recently looked at in Maps, newest first. This is Maps' Recents list, \" +\n \"not a search history: it holds places, directions and searches the user actually \" +\n \"opened. Needs Full Disk Access.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.places(\"history\", { limit: limit ?? client.config.maxResults });\n const named = result.places.filter((p) => p.name).length;\n return ok(\n compact({\n recents: result.places,\n count: result.places.length,\n // MEASURED: 1 of 33 rows carried a name. A recent's place name\n // lives in the linked map item's undecoded blob, not in a column,\n // so most rows have coordinates and no label. Saying so stops a\n // caller reporting \"33 unnamed places\" as if the data were damaged.\n namesUnavailable:\n result.places.length > 0 && named * 4 < result.places.length\n ? `Only ${named} of ${result.places.length} entries carry a name. Maps keeps a ` +\n `recent's place name in an encoded record this server does not decode, so most ` +\n `rows have coordinates and dates but no label. This is the store's shape, not ` +\n `missing data.`\n : undefined,\n truncated: result.truncated\n ? `More entries exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n datesUnavailable: result.datesAvailable\n ? undefined\n : \"Timestamps could not be placed on a known epoch, so every date reads null.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_search_places\",\n {\n description:\n \"Search every saved place — favourites, collection entries and recents — by name, by \" +\n \"the label the user gave it, or by address. Returns each match with the kind of entry \" +\n \"it came from. Needs Full Disk Access. This searches what is SAVED on this Mac; it \" +\n \"does not search Apple's map of the world.\",\n inputSchema: { query: queryArg, limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ query, limit }) =>\n wrapResult(async () => {\n const result = client.search({ query, limit: limit ?? client.config.maxResults });\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More matches exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_get_place\",\n {\n description:\n \"Get one saved place by its ref, with coordinates, address and dates. Needs Full Disk \" +\n \"Access.\",\n inputSchema: { ref: placeRefArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ ref }) =>\n wrapResult(async () => {\n const { kind, key } = decodePlaceRef(ref);\n const place = client.place(kind, key);\n if (!place) {\n return fail(\n \"uuid\" in key\n ? `No place for that ref. It was removed in Maps, or it belongs to a different ` +\n `store than the one being read.`\n : `No place for that ref. It may have been removed in Maps, or iCloud may have ` +\n `re-synced the store and renumbered the rows since the listing ran — this ref ` +\n `carries a row id, which only this store's current numbering can resolve. ` +\n `Re-run the listing for a current ref.`,\n );\n }\n return ok({ place });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { decodePlaceRef } from \"../client/ref.js\";\nimport { compact, fail, ok, placeRefArg, wrapResult } from \"./util.js\";\n\n/**\n * The mutating tools.\n *\n * Registered only when `APPLE_MAPS_ALLOW_WRITES` is true, so a host that has not\n * opted in is never told they exist — the same gate every other surface uses.\n *\n * ## Two things these descriptions must say, because they are surprising\n *\n * **Adding a favourite leaves an entry in Recents.** A place is only real to\n * Maps once it has a GEO record, and the only thing that can produce one is Maps\n * itself: the place is opened through the `maps://` URL scheme, Maps resolves it\n * and files it in Recents, and that record is copied into the favourite. There\n * is no way to have the first without the second, so the caller is told rather\n * than surprised. When the place is already known to the store, no seeding\n * happens and no Recents entry appears.\n *\n * **It reaches the user's other devices.** The store is mirrored by\n * `NSPersistentCloudKitContainer`, which reconciles on the app's next save\n * whether or not it was told anything. A favourite added here arrived on an\n * iPhone. That makes this the one write in the bundle whose blast radius is\n * larger than the machine it runs on, and the descriptions say so.\n */\nexport const registerWriteTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_add_favorite\",\n {\n description:\n \"Save a place to Maps' favourites (the Pinned list). Give the place NAME as `query` — a \" +\n \"bare coordinate does not identify a place to Maps — and latitude/longitude when known, \" +\n \"which makes the match exact and skips the lookup. SIDE EFFECT: unless the place is \" +\n \"already in the store, Maps is asked to resolve it and the place also appears in the \" +\n \"user's Recents; there is no way to add a favourite without that. The favourite syncs \" +\n \"to the user's other Apple devices through iCloud. Idempotent: asking twice for the same \" +\n \"place returns the existing favourite rather than creating a second. Needs Full Disk \" +\n \"Access.\",\n inputSchema: {\n query: z\n .string()\n .min(1)\n .describe(\"The place's name, as you would type it into Maps' search field.\"),\n latitude: z.number().min(-90).max(90).optional().describe(\"Latitude, when known.\"),\n longitude: z.number().min(-180).max(180).optional().describe(\"Longitude, when known.\"),\n name: z.string().min(1).optional().describe(\"Label to save it under. Defaults to `query`.\"),\n },\n annotations: { readOnlyHint: false, idempotentHint: true },\n },\n async ({ query, latitude, longitude, name }) =>\n wrapResult(async () => {\n const result = client.writer().addFavorite({ query, latitude, longitude, name });\n return ok(\n compact({\n favorite: {\n ref: `p1:f:${result.uuid.replaceAll(\"-\", \"\")}`,\n name: result.name,\n latitude: result.latitude,\n longitude: result.longitude,\n },\n created: result.created,\n alreadyExisted: result.created\n ? undefined\n : \"A favourite for this place already existed and was returned unchanged.\",\n recentsNote: result.seeded\n ? \"Maps was asked to resolve this place, so it now also appears in the user's \" +\n \"Recents. That is how the place record is obtained and cannot be avoided.\"\n : undefined,\n syncNote:\n \"This favourite will reach the user's other Apple devices through iCloud once \" +\n \"Maps next runs.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_remove_favorite\",\n {\n description:\n \"Remove a place from Maps' favourites, by a ref from apple_maps_list_favorites. This \" +\n \"deletes the favourite on the user's other Apple devices too, through iCloud. It does \" +\n \"not affect Guides, Recents, or anything else that references the same place. Needs \" +\n \"Full Disk Access.\",\n inputSchema: { ref: placeRefArg },\n annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true },\n },\n async ({ ref }) =>\n wrapResult(async () => {\n const { kind, key } = decodePlaceRef(ref);\n if (kind !== \"favorite\") {\n return fail(\n `That ref points at a ${kind === \"history\" ? \"recent\" : \"collection entry\"}, not a ` +\n `favourite. Only favourites can be removed; pass a ref from ` +\n `apple_maps_list_favorites.`,\n );\n }\n const removed = client.writer().removeFavorite(key);\n if (!removed) {\n return fail(\n \"No favourite matched that ref. It may already have been removed, or the listing \" +\n \"it came from may predate a change. Re-run apple_maps_list_favorites.\",\n );\n }\n return ok({\n removed: true,\n syncNote:\n \"The removal will reach the user's other Apple devices through iCloud once Maps \" +\n \"next runs.\",\n });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerPlaceTools } from \"./places.js\";\nimport { registerWriteTools } from \"./writes.js\";\n\nexport type ToolContext = {\n /**\n * Gates the two mutating tools, which are NOT registered when it is false —\n * so a host that has not opted in is never told they exist.\n *\n * This surface writes SQL directly into Maps' Core Data store, because Maps\n * has no scripting dictionary and no App Intents to write through. The store\n * is mirrored to iCloud, so a write here reaches every device on the account.\n * `docs/maps.md` carries the measurements; `client/write.ts` carries the rule\n * that makes it safe — never fabricate a place record, only copy one Maps\n * wrote itself.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Maps tools.\n *\n * The registered set does NOT vary with whether the store opened. That is a\n * runtime condition and MCP clients cache the tool list, so a list that shrank\n * without Full Disk Access would stay shrunk after the grant was given. Every\n * tool is registered on a machine with no grant at all; each fails with an\n * error that says which permission is missing.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleMapsClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client);\n registerPlaceTools(server, client);\n if (!ctx.allowWrites) return;\n registerWriteTools(server, client);\n};\n","import { registerSurfaceResources, type Logger } from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleMapsClient } from \"./client/maps.js\";\nimport type { Config } from \"./config.js\";\nimport { MAPS_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 discovery never reaches the developer's real home. */\n home?: string;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleMapsClient;\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 *\n * There is no `osascript` seam here, unlike every other surface. This server\n * never spawns one — Maps is not scriptable, so there is no Apple Events lane\n * to inject a fake for. Its absence is the point.\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 AppleMapsClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\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://maps/guide` that this\n * server does not serve.\n */\n if (config.exposePrompts) {\n registerPrompts(server);\n registerSurfaceResources(server, {\n surface: \"maps\",\n displayName: \"Maps\",\n guide: MAPS_GUIDE,\n diagnostics: () => buildDiagnostics(client),\n // No inventory. Favourites, collections and recents are three queries\n // rather than three named containers, and a caller does not address them\n // by name the way they address a mailbox or a folder.\n });\n }\n\n return { server, client };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server-Bum_vDw3.js","names":["#col","#colText","#unfiledClause","#entityFor","#placeSelect","#toPlace","#keyClause","#path","#openUrl","#seedTimeoutMs","#existingFavorite","#donorNear","#maxHistory","#seededDonor","#open","#entity","#nextPk","#config","#logger","#home","#located","#store","#storeError","#render","#renderCollection"],"sources":["../src/build-info.ts","../src/client/dates.ts","../src/client/errors.ts","../src/client/locate.ts","../src/client/ref.ts","../src/client/store.ts","../src/client/write.ts","../src/client/maps.ts","../src/config.ts","../src/guide.ts","../src/prompts.ts","../src/tools/util.ts","../src/tools/diagnostics.ts","../src/tools/places.ts","../src/tools/writes.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-maps\",\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 * Date handling for the Maps tools.\n *\n * Deliberately much smaller than `packages/safari/src/client/dates.ts`. That\n * surface needed a whole input grammar because history is a range query; Maps'\n * entities are lists a person curates, and v1 ships no tool taking a date\n * argument. So this module solves only the OUTPUT half — turning a stored\n * number into an instant. Inventing a parser nothing calls would be a fourth\n * copy of a grammar `packages/safari` has already noted should be hoisted into\n * core rather than duplicated again.\n *\n * ## The epoch is DETECTED, never assumed\n *\n * `docs/maps.md` records the measurement: `ZCREATETIME` resolves to\n * **apple-seconds** (Core Data, 2001 anchor). The same value read as unix\n * seconds lands in **1995** — well-formed, plausible at a glance, and wrong by\n * 31 years. `pnpm probe:maps` prints both readings side by side precisely\n * because the wrong one does not announce itself.\n *\n * So the offset is resolved from the store's own newest timestamp at open time,\n * and when it cannot be resolved every date renders `null` rather than being\n * guessed. An absent timestamp is a visible gap somebody can report; a\n * confidently wrong one is not. This is the discipline `packages/safari`\n * arrived at after a probe misread its own column — adopted here rather than\n * re-derived.\n */\n\nimport { CORE_DATA_EPOCH_OFFSET, detectEpoch } from \"@mgcrea/mcp-apple-core\";\n\nexport { CORE_DATA_EPOCH_OFFSET };\n\n/**\n * How this store's timestamps map onto real time.\n *\n * `confident` is the field that matters. `detectEpoch` always returns an offset\n * — it falls back to unix when nothing fits — so the offset alone cannot\n * distinguish \"measured as unix\" from \"gave up and assumed unix\". Rendering the\n * second as though it were the first is the failure this module exists to\n * prevent, so the two are kept apart.\n */\nexport type Epoch = {\n offset: number;\n reason: string;\n confident: boolean;\n};\n\n/** What `detectEpoch` says when it has matched nothing. Same shape as Safari's. */\nconst GAVE_UP = /^(no dated rows|neither epoch)/;\n\nexport const resolveEpoch = (maxTimestamp: number | null, now: number = Date.now()): Epoch => {\n const { offset, reason } = detectEpoch(maxTimestamp, now);\n return { offset, reason, confident: !GAVE_UP.test(reason) };\n};\n\n/** The expectation docs/maps.md carries, used only where no store is open. */\nexport const APPLE_SECONDS: Epoch = {\n offset: CORE_DATA_EPOCH_OFFSET,\n reason: \"assumed apple-seconds; no store was opened to measure against\",\n confident: false,\n};\n\n/** A stored timestamp to a JS Date, or null when it cannot be placed. */\nexport const fromStoreTime = (value: number | null, epoch: Epoch): Date | null => {\n if (value === null || !Number.isFinite(value) || value === 0) return null;\n if (!epoch.confident) return null;\n const date = new Date((value + epoch.offset) * 1000);\n return Number.isNaN(date.getTime()) ? null : date;\n};\n\n/** ISO-8601, or null. What every date field on a result carries. */\nexport const renderInstant = (value: number | null, epoch: Epoch): string | null =>\n fromStoreTime(value, epoch)?.toISOString() ?? null;\n","/**\n * Maps' error surface. The taxonomy lives in `@mgcrea/mcp-apple-core`; what\n * belongs here is the identity those messages are written against, plus the\n * failures no other surface has.\n */\n\nimport { AppleAutomationError, type SurfaceContext } from \"@mgcrea/mcp-apple-core\";\n\nexport const MAPS_SURFACE: SurfaceContext = {\n appName: \"Maps\",\n envPrefix: \"APPLE_MAPS\",\n};\n\n/**\n * Maps is one of the few Apple apps whose bundle id matches its display name —\n * unlike `com.apple.iCal`, `com.apple.AddressBook` and `com.apple.MobileSMS`.\n * Stated rather than assumed, because this repo has been caught by the opposite\n * three times.\n *\n * It is recorded here and used by almost nothing. Maps ships **no scripting\n * dictionary** — `/System/Applications/Maps.app` contains no `.sdef`, checked\n * directly rather than inferred from `NSAppleScriptEnabled` — so there is no\n * Apple Events lane to address, and this server never sends one.\n */\nexport const MAPS_BUNDLE_ID = \"com.apple.Maps\";\n\nexport {\n AppleAutomationError as AppleMapsError,\n IndexUnavailableError,\n PlatformError,\n PreconditionError,\n ProtocolError,\n SchemaDriftError,\n TccDeniedError,\n WritesDisabledError,\n} from \"@mgcrea/mcp-apple-core\";\n\n/**\n * The store could not be read.\n *\n * This surface has **one lane and no fallback**, which puts it with Messages\n * rather than with Safari: without Full Disk Access there is no degraded Maps\n * server, there is no Maps server. So every read throws this rather than\n * returning an empty list — an empty `favorites` reads exactly like a person\n * who has saved no places, and that is the failure this error exists to\n * prevent.\n *\n * The hint names the trap that actually catches people: the store lives at a\n * path with **no file extension**, inside the one directory in Maps' container\n * that Full Disk Access gates. A sweep for `*.db` finds nothing and concludes\n * the data is not on disk. It is.\n */\nexport class MapsStoreUnavailableError extends AppleAutomationError {\n override readonly name = \"MapsStoreUnavailableError\";\n\n constructor(reason: string) {\n super(\n `${reason} Maps has no second lane — it ships no scripting dictionary, so there is no ` +\n `Apple Events fallback and nothing at all can be read without the grant.`,\n {},\n );\n }\n}\n\n/** A ref no longer resolves — the place was removed, or the store was re-synced. */\nexport class PlaceNotFoundError extends AppleAutomationError {\n override readonly name = \"PlaceNotFoundError\";\n\n constructor(ref: string) {\n super(\n `No place for ref \"${ref}\". It may have been removed in Maps, or iCloud may have ` +\n `re-synced the store and renumbered it. Re-run the listing to get a current ref.`,\n { ref },\n );\n }\n}\n\n/**\n * The epoch could not be identified from the data.\n *\n * The same discipline `packages/safari` applies, for the same measured reason:\n * a wrong epoch produces dates that are well-formed and wrong by 31 years.\n * `docs/maps.md` records that this store's `ZCREATETIME` read as apple-seconds\n * and that a unix-seconds reading of the same value lands in 1995 while still\n * looking entirely plausible. So the offset is detected from the store at open\n * time and dates are WITHHELD when it cannot be.\n */\nexport class UndatableStoreError extends AppleAutomationError {\n override readonly name = \"UndatableStoreError\";\n\n constructor(reason: string) {\n super(\n `Maps' timestamps could not be placed on a known epoch (${reason}). Dates are withheld ` +\n `rather than guessed — a timestamp wrong by decades reads exactly like a correct one. ` +\n `Everything that does not depend on a date still works.`,\n {},\n );\n }\n}\n","import { readdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport {\n describeStore,\n inspectFile,\n type FileFacts,\n type StoreFacts,\n} from \"@mgcrea/mcp-apple-core\";\n\n/**\n * Find Maps' store.\n *\n * ## The path, and the three ways it was missed\n *\n * `~/Library/Containers/com.apple.Maps/Data/Maps/MapsSync_0.0.1` — 3.3 MB of\n * Core Data on the probed machine. `docs/maps.md` records that this surface was\n * declared \"no file lane\" three separate times before the file was found, and\n * each failure is a rule this locator is shaped by:\n *\n * 1. **The file has no extension.** A sweep for `*.db` / `*.sqlite*` finds\n * nothing here. Nothing in this file matches on a suffix.\n * 2. **`Data/Maps/` is gated.** It was the ONE unreadable directory in the\n * container, so a listing taken without Full Disk Access omits it, and the\n * omission reads as absence. `inspectFile` splits exists from readable\n * precisely so those two never collapse into one answer.\n * 3. **`group.com.apple.Maps` is a decoy.** It exists, it is EPERM without the\n * grant, and it holds three files that are not the store. A probe that\n * could not descend into it reported it as empty.\n *\n * The rule underneath all three is the one `docs/surfaces.md` states: \"'Absent'\n * and 'EPERM' are different findings.\" Keeping them apart, and saying which one\n * happened, is most of this module's job.\n *\n * ## The device-local cache is located and never read\n *\n * `MapsSync_0.0.1_deviceLocalCache.db` sits beside the store with the same 33\n * entities and, on the probed machine, **zero rows in every one of them**. It is\n * reported by diagnostics so anyone comparing the directory against this\n * server's output can see it was considered, and it is never opened.\n */\n\n/** The container path. `Data/Maps` is the gated directory, not `Data/Library`. */\nexport const MAPS_DIR = [\"Library\", \"Containers\", \"com.apple.Maps\", \"Data\", \"Maps\"];\n\n/**\n * No extension, and a version in the name.\n *\n * The `0.0.1` was stable on every machine this project has measured, but it is a\n * version string and it will move. `locateStore` falls back to scanning for a\n * `MapsSync_*` sibling rather than reporting a rename as \"Maps has never run\".\n */\nexport const STORE_FILENAME = \"MapsSync_0.0.1\";\nexport const STORE_PREFIX = \"MapsSync_\";\nexport const LOCAL_CACHE_SUFFIX = \"_deviceLocalCache.db\";\n\nexport const FDA_HINT =\n \"Grant Full Disk Access to the app running this server (System Settings > Privacy & \" +\n \"Security > Full Disk Access) and restart it. Granting it to Maps.app does nothing — the \" +\n \"reader needs the permission, not the app.\";\n\nexport type LocatedFile = FileFacts & { path: string };\n\nexport type LocateResult = StoreFacts & {\n directory: string;\n storePath: string | null;\n /** Reported so a reader can see it was considered. Never opened. */\n localCache: LocatedFile;\n /** True when the name came from a directory scan rather than the constant. */\n resolvedByScan: boolean;\n reason: string | null;\n};\n\nexport const defaultDirectory = (home: string = homedir()): string => join(home, ...MAPS_DIR);\n\nexport const defaultStorePath = (home: string = homedir()): string =>\n join(defaultDirectory(home), STORE_FILENAME);\n\nconst locateFile = (path: string): LocatedFile => ({ ...inspectFile(path), path });\n\n/**\n * Look for a `MapsSync_*` that is neither the local cache nor a SQLite sidecar.\n *\n * Only reached when the constant path is absent, and it needs the directory to\n * be listable — the same grant the store itself needs, so this asks for no new\n * permission and simply survives a version bump.\n */\nconst scanForStore = (directory: string, readdir: (p: string) => string[]): string | null => {\n let names: string[];\n try {\n names = readdir(directory);\n } catch {\n return null;\n }\n const candidate = names\n .filter(\n (n) =>\n n.startsWith(STORE_PREFIX) &&\n !n.endsWith(LOCAL_CACHE_SUFFIX) &&\n !n.endsWith(\"-wal\") &&\n !n.endsWith(\"-shm\"),\n )\n .toSorted()\n .at(-1);\n return candidate ? join(directory, candidate) : null;\n};\n\nexport const locateStore = (\n opts: {\n storePath?: string | undefined;\n home?: string;\n /** Injected by tests so a scan never reaches a real directory. */\n readdir?: (path: string) => string[];\n } = {},\n): LocateResult => {\n const directory = defaultDirectory(opts.home);\n const readdir = opts.readdir ?? ((p: string): string[] => readdirSync(p));\n\n const explicit = opts.storePath;\n const constant = defaultStorePath(opts.home);\n\n let storePath = explicit ?? constant;\n let resolvedByScan = false;\n\n if (!explicit && !describeStore(constant).exists) {\n const scanned = scanForStore(directory, readdir);\n if (scanned) {\n storePath = scanned;\n resolvedByScan = true;\n }\n }\n\n const facts = describeStore(storePath);\n\n const reason = facts.readable\n ? null\n : facts.exists\n ? `Found Maps' store at ${storePath} but cannot read it. ${FDA_HINT}`\n : explicit\n ? `No file at ${explicit}. APPLE_MAPS_STORE points at nothing.`\n : `No Maps store at ${storePath}, and no MapsSync_* file in ${directory}. Either Maps ` +\n `has never been used on this account, or the directory itself is not readable — those ` +\n `look identical from outside and only the grant tells them apart. ${FDA_HINT}`;\n\n return {\n ...facts,\n directory,\n storePath: facts.exists || !explicit ? storePath : null,\n localCache: locateFile(`${constant}${LOCAL_CACHE_SUFFIX}`),\n resolvedByScan,\n reason,\n };\n};\n","import { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\nimport type { EntityKey } from \"./store.js\";\n\n/**\n * Refs for places and collections.\n *\n * ## Why the ref is not `ZMUID`\n *\n * `ZMUID` is Apple's own place identifier and looks like the better key — it is\n * the same number for the same restaurant across every device. It is not used,\n * for two measured reasons recorded in `docs/maps.md`:\n *\n * * It is populated **20 of 23** favourites. The three without it are the\n * rows that have no linked place at all, but a ref scheme that cannot\n * address three rows in twenty-three is not a ref scheme.\n * * It identifies a PLACE, not an entry. The same café saved as a favourite\n * AND filed in a collection carries one `ZMUID` across both, so a ref built\n * on it could not say which of the two a caller meant.\n *\n * The keys below address the ENTRY, which is what every tool here returns. The\n * kind is carried in the prefix so a favourite ref and a collection-item ref\n * can never be confused for one another.\n *\n * ## What the ref carries: a uuid when the store has one, a row id when it does not\n *\n * `ZIDENTIFIER` — a 16-byte Core Data UUID — is set on every favourite,\n * collection, collection item and recent on a real store, and is distinct on\n * every one. It addresses the ENTRY, survives a delete elsewhere in the table,\n * and survives an iCloud re-sync renumbering rows. It is the ref whenever\n * `store.ts` can confirm total coverage.\n *\n * It was found by watching Maps write, not by reading the schema — see\n * `docs/maps.md`. The read probe never reported it, because a probe can only\n * report columns it thought to look for.\n *\n * When a store has no usable identifier the ref falls back to the Core Data row\n * id, and then the old caveat applies in full: `Z_PK` is reused after a delete\n * and renumbered by a re-sync, so such a ref is only good for the session.\n * `packages/messages` rejected row ids outright for that reason; here they are\n * the degraded mode rather than the design.\n *\n * The two are told apart BY SHAPE — a uuid is 32 hex characters, a row id is\n * decimal — and the resolver refuses to try a uuid against a store with no\n * identifier column rather than reinterpreting it as a number. Silently\n * resolving one key space in the other would find a real but wrong place, which\n * is worse than finding nothing.\n *\n * ## Why `p1:` and `pc1:`\n *\n * `c1:` is Calendar's, `r1:` Reminders', `k1:` Contacts', `n1:` Notes',\n * `m1:`/`mc1:` Messages', `s1:`/`sb1:` Safari's. A ref that decodes under two\n * surfaces is worse than one that decodes under none, so each prefix is claimed\n * once and the version digit keeps a future change additive rather than a\n * silent reinterpretation of refs already sitting in a conversation.\n */\n\nexport const PLACE_REF_VERSION = \"p1\";\nexport const COLLECTION_REF_VERSION = \"pc1\";\n\n/** The entity a place ref points into. Carried in the ref, never guessed. */\nexport type PlaceKind = \"favorite\" | \"collection-item\" | \"history\";\n\nconst KIND_CODE: Record<PlaceKind, string> = {\n favorite: \"f\",\n \"collection-item\": \"c\",\n history: \"h\",\n};\nconst CODE_KIND: Record<string, PlaceKind> = {\n f: \"favorite\",\n c: \"collection-item\",\n h: \"history\",\n};\n\nconst PLACE_PATTERN = /^p1:([fch]):([0-9a-f]{32}|\\d+)$/;\nconst COLLECTION_PATTERN = /^pc1:([0-9a-f]{32}|\\d+)$/;\n\n/** 32 hex characters is a uuid; anything else that matched the pattern is a row id. */\nconst toKey = (raw: string): EntityKey =>\n /^[0-9a-f]{32}$/.test(raw)\n ? {\n uuid: `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`,\n }\n : { rowId: Number(raw) };\n\n/** A uuid goes into the ref undashed, so the ref stays one opaque token. */\nconst fromKey = (key: EntityKey): string =>\n \"uuid\" in key ? key.uuid.replaceAll(\"-\", \"\").toLowerCase() : String(key.rowId);\n\nconst otherSurface = (raw: string): string => {\n if (raw.startsWith(\"c1:\")) return \" That one is a Calendar event ref.\";\n if (raw.startsWith(\"r1:\")) return \" That one is a Reminders ref.\";\n if (raw.startsWith(\"k1:\")) return \" That one is a Contacts ref.\";\n if (raw.startsWith(\"n1:\")) return \" That one is a Notes ref.\";\n if (raw.startsWith(\"mc1:\")) return \" That one is a Messages chat ref.\";\n if (raw.startsWith(\"m1:\")) return \" That one is a Messages message ref.\";\n if (raw.startsWith(\"sb1:\")) return \" That one is a Safari bookmark ref.\";\n if (raw.startsWith(\"s1:\")) return \" That one is a Safari history ref.\";\n if (raw.startsWith(\"pc1:\")) return \" That one is a COLLECTION ref — this wants a place ref.\";\n if (raw.startsWith(\"p1:\")) return \" That one is a PLACE ref — this wants a collection ref.\";\n return \"\";\n};\n\nexport class InvalidMapsRefError extends AppleAutomationError {\n override readonly name = \"InvalidMapsRefError\";\n\n constructor(raw: string, want: \"place\" | \"collection\") {\n const shape = want === \"place\" ? '\"p1:<kind>:<id>\"' : '\"pc1:<id>\"';\n super(\n `\"${raw}\" is not a ${want} ref. Refs come from apple_maps_* results and look like ` +\n `${shape} — they are opaque and must not be constructed by hand.${otherSurface(raw)}`,\n { ref: raw },\n );\n }\n}\n\nexport const encodePlaceRef = (kind: PlaceKind, key: EntityKey): string =>\n `${PLACE_REF_VERSION}:${KIND_CODE[kind]}:${fromKey(key)}`;\n\nexport const encodeCollectionRef = (key: EntityKey): string =>\n `${COLLECTION_REF_VERSION}:${fromKey(key)}`;\n\nexport const decodePlaceRef = (raw: string): { kind: PlaceKind; key: EntityKey } => {\n const m = PLACE_PATTERN.exec(raw.trim());\n const kind = m?.[1] ? CODE_KIND[m[1]] : undefined;\n if (!m?.[2] || !kind) throw new InvalidMapsRefError(raw, \"place\");\n return { kind, key: toKey(m[2]) };\n};\n\nexport const decodeCollectionRef = (raw: string): EntityKey => {\n const m = COLLECTION_PATTERN.exec(raw.trim());\n if (!m?.[1]) throw new InvalidMapsRefError(raw, \"collection\");\n return toKey(m[1]);\n};\n","import type { DatabaseSync } from \"node:sqlite\";\n\nimport {\n columnsOf,\n escapeLike,\n fingerprintSchema,\n openReadOnly,\n SchemaDriftError,\n type Logger,\n type ReadOnlyMode,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { resolveEpoch, type Epoch } from \"./dates.js\";\n\n/**\n * Maps' file lane. There is no other lane.\n *\n * ## What is measured, and what is NOT\n *\n * MEASURED by `pnpm probe:maps` on macOS 26.6 — 146 schema objects, fingerprint\n * `2bbc03143125`, Core Data, `ZCREATETIME` on apple-seconds:\n *\n * ZFAVORITEITEM 24 rows ZCOLLECTION 10 rows\n * ZCOLLECTIONITEM 30 rows ZHISTORYITEM 34 rows\n * ZMIXINMAPITEM 71 rows ZREVIEWEDPLACE 24 rows\n *\n * and the id bridge, verified by running the join rather than by reading column\n * names: `ZMIXINMAPITEM`'s inverse relationships partition exactly —\n * 30 collection items + 21 favourites + 20 history rows = 71 = every row.\n *\n * NOT measured: this file has never been run against the real store by its\n * author, who has no Full Disk Access. Everything below is written against a\n * probe REPORT, which is one step further from the data than\n * `packages/safari/src/client/store.ts` was, and that file already treats every\n * column as optional. This one goes further in two ways.\n *\n * ## Columns are resolved BY COVERAGE, not by first name match\n *\n * The probe found `ZHISTORYITEM` carrying both `ZLATITUDE` (1 of 33 rows) and\n * `ZLATITUDE1` (19 of 33). A resolver that took the first candidate it\n * recognised would pick the column that is null 97% of the time and report that\n * Maps holds almost no coordinates. So each logical field names several\n * candidates and the one with the **most non-null values** wins, counted at\n * open time. That is a real query per candidate, run once, on tables of tens of\n * rows.\n *\n * ## Collection membership is a JOIN TABLE, and it is VALIDATED not guessed\n *\n * MEASURED: membership is `Z_6PLACES(Z_6COLLECTIONS, Z_7PLACES)`, a Core Data\n * many-to-many. `Z_PRIMARYKEY` decodes ordinal 6 as Collection and 7 as\n * CollectionItem, so the relationship is `Collection.places`.\n *\n * Four column names were guessed here before it was found — ZCOLLECTION,\n * ZCOLLECTION1, ZPARENTCOLLECTION, ZOWNINGCOLLECTION — and the store has none\n * of them, so every guide listed empty. A many-to-many leaves NO COLUMN on\n * either entity, so no list of column names could have contained the answer.\n * The near-miss is what makes this worth stating: `ZCOLLECTIONITEM.ZMAPITEM`\n * joins `ZCOLLECTION` for 3 of 10 collections, and a resolver picking the\n * best-covered joinable column would have chosen it and been confidently wrong.\n *\n * So membership is resolved BY RUNNING THE JOIN and scoring it against an\n * oracle the store hands over for free: `ZCOLLECTION.ZPLACESCOUNT`, Maps' own\n * count per guide. A candidate is accepted only when it reproduces all ten\n * numbers exactly with no key pointing at a missing collection. That survives\n * Apple renaming the relationship, which a hard-coded `Z_6PLACES` would not.\n *\n * The oracle is not independent evidence, and it is stronger for it. Core Data\n * maintains `ZPLACESCOUNT` with a trigger that reads:\n *\n * UPDATE ZCOLLECTION SET ZPLACESCOUNT =\n * (SELECT IFNULL(COUNT(Z_6COLLECTIONS), 0)\n * FROM Z_6PLACES WHERE Z_6COLLECTIONS = NEW.Z_PK)\n *\n * so Apple's own schema states the relationship this resolver re-derives. The\n * count match is therefore guaranteed for the true mechanism rather than lucky,\n * and coincidental for anything else. The triggers are visible only in the\n * captured fixture — `pnpm probe:maps --write` omits them from the replay\n * because they call Core Data's private SQLite functions, which is recorded in\n * `writeFixture`.\n *\n * Because it is many-to-many, one place can sit in several guides, and the\n * membership query uses `IN (SELECT ...)` rather than a JOIN so a place in two\n * guides is not returned twice from one of them.\n *\n * MEASURED: 30 item rows, 18 of them in a guide. The other 12 belong to no\n * collection at all; all 30 link to a `ZMIXINMAPITEM`, so they are intact\n * places rather than broken rows.\n *\n * `SchemaDriftError` fires for exactly one condition — none of the three\n * place-bearing tables exists — because without them there is no surface.\n * Everything else is a capability downgrade that is reported, never a throw.\n */\n\nexport const FAVORITES_TABLE = \"ZFAVORITEITEM\";\nexport const COLLECTIONS_TABLE = \"ZCOLLECTION\";\nexport const COLLECTION_ITEMS_TABLE = \"ZCOLLECTIONITEM\";\nexport const HISTORY_TABLE = \"ZHISTORYITEM\";\nexport const MAP_ITEMS_TABLE = \"ZMIXINMAPITEM\";\n\n/**\n * Candidates per logical field, best-known first — but order only breaks ties.\n * Coverage decides. See the header.\n */\nconst NAME_CANDIDATES = [\"ZMAPITEMNAME\", \"ZCUSTOMNAME\", \"ZLOCATIONDISPLAY\", \"ZTITLE\"] as const;\nconst LAT_CANDIDATES = [\"ZLATITUDE\", \"ZLATITUDE1\"] as const;\nconst LON_CANDIDATES = [\"ZLONGITUDE\", \"ZLONGITUDE1\"] as const;\nconst ADDRESS_CANDIDATES = [\"ZMAPITEMADDRESS\", \"ZORIGINATINGADDRESSSTRING\"] as const;\nconst MUID_CANDIDATES = [\"ZMUID\"] as const;\nconst MAP_ITEM_FK_CANDIDATES = [\"ZMAPITEM\"] as const;\nconst CREATED_CANDIDATES = [\"ZCREATETIME\"] as const;\nconst MODIFIED_CANDIDATES = [\"ZMODIFICATIONTIME\"] as const;\nconst COLLECTION_TITLE_CANDIDATES = [\"ZTITLE\", \"ZNAME\", \"ZCUSTOMNAME\"] as const;\n\n/**\n * The stable per-entry identifier, if this store has one.\n *\n * FOUND BY WATCHING MAPS WRITE, not by reading the schema: every row Maps\n * created during `pnpm probe:maps-write` carried `ZIDENTIFIER`, a 16-byte blob\n * — a UUID. It is not in the read probe's report because that probe only looked\n * for columns it already had candidates for, which is a good argument for\n * diffing a live write even when the read surface already works.\n *\n * `ZORIGINALIDENTIFIER` is deliberately NOT a candidate. It exists on\n * `ZCOLLECTIONITEM` and is populated on 3 rows of 30 — it records where an entry\n * was copied FROM, so it is neither complete nor unique to the entry.\n */\nconst IDENTIFIER_CANDIDATES = [\"ZIDENTIFIER\"] as const;\n\n/**\n * Maps' own count of the places in a guide, and the oracle every membership\n * candidate is scored against. See the header.\n */\nconst COLLECTION_COUNT_COLUMN = \"ZPLACESCOUNT\";\n\n/** Core Data's own tables, which are never a relationship. */\nconst RESERVED_JOIN_TABLES = new Set([\"Z_METADATA\", \"Z_MODELCACHE\", \"Z_PRIMARYKEY\"]);\n\nexport type FieldMap = {\n /**\n * The column holding a stable UUID, or null when this store has none good\n * enough to address rows by. See `resolveIdentifier`.\n */\n identifier: string | null;\n name: string | null;\n customName: string | null;\n latitude: string | null;\n longitude: string | null;\n address: string | null;\n muid: string | null;\n mapItem: string | null;\n created: string | null;\n modified: string | null;\n};\n\nexport type EntityFacts = {\n table: string;\n present: boolean;\n rows: number;\n columns: Set<string>;\n fields: FieldMap;\n};\n\n/**\n * How an item says which collection it is in, once proved against the oracle.\n *\n * Two shapes because Core Data has two: a scalar foreign key named after the\n * relationship, and a `Z_<ordinal><RELATIONSHIP>` join table for a\n * many-to-many. THIS store uses the second. Both are carried because the\n * resolver proves whichever is there rather than assuming, and a store that\n * changes shape should degrade rather than read the wrong column.\n */\nexport type CollectionMembership =\n | { kind: \"column\"; column: string }\n | { kind: \"joinTable\"; table: string; collectionColumn: string; itemColumn: string };\n\nexport type StoreCapabilities = {\n fingerprint: string;\n tables: string[];\n favorites: EntityFacts;\n collections: EntityFacts;\n collectionItems: EntityFacts;\n history: EntityFacts;\n mapItems: EntityFacts;\n /**\n * The proved membership mechanism, or null when nothing reproduced\n * `ZPLACESCOUNT` and collections must therefore list without their places.\n */\n membership: CollectionMembership | null;\n /**\n * The membership mechanism as one short string, for diagnostics.\n *\n * Kept because `apple_maps_diagnostics` reported `collectionFk` before this\n * was understood, and a field that silently changes meaning is worse than one\n * that widens: it now names a join table as well as a column.\n */\n collectionFk: string | null;\n epoch: Epoch;\n};\n\nexport type PlaceRow = {\n id: number;\n /**\n * The stable identifier, when the store has one. Null means refs on this\n * entity fall back to the row id and are only good for the session.\n */\n uuid: string | null;\n name: string | null;\n customName: string | null;\n latitude: number | null;\n longitude: number | null;\n address: string | null;\n /**\n * Apple's place id, carried as a STRING.\n *\n * MEASURED on a real store: `-2679868148951248105`. It is a 64-bit integer and\n * `node:sqlite` THROWS on one past `Number.MAX_SAFE_INTEGER` rather than\n * truncating, so reading it as a number failed the whole listing with\n * \"Value is too large to be represented as a JavaScript number\" — one column\n * taking down every favourite. `docs/surfaces.md` states the rule this broke:\n * read such columns as BigInt or `CAST(... AS TEXT)`.\n *\n * The fixture used a small id, so the offline suite passed. Only the real\n * store has ids of this size.\n */\n muid: string | null;\n createdRaw: number | null;\n modifiedRaw: number | null;\n /**\n * False when the row has no linked `ZMIXINMAPITEM`.\n *\n * MEASURED: 3 of 23 favourites, and they carry no name and no coordinate\n * either. Almost certainly the unconfigured Home / Work / School slots that\n * Maps creates whether or not anyone fills them in. They are RETURNED rather\n * than filtered, with this flag, because silently dropping rows is how a\n * caller concludes a favourite was deleted.\n */\n linked: boolean;\n};\n\nexport type CollectionRow = {\n id: number;\n uuid: string | null;\n title: string | null;\n placesCount: number | null;\n createdRaw: number | null;\n modifiedRaw: number | null;\n};\n\n/**\n * How a caller addresses one row.\n *\n * Two shapes rather than one because the store decides which is available, not\n * the caller: a store whose `ZIDENTIFIER` is complete gets durable refs, one\n * without falls back to row ids. Making the difference explicit in the type\n * means a resolver cannot quietly treat a row id as a uuid when the store\n * changed underneath it.\n */\nexport type EntityKey = { uuid: string } | { rowId: number };\n\nconst isRealTable = (name: string): boolean => !name.startsWith(\"sqlite_\");\n\n/**\n * `HEX()` of a 16-byte blob to a canonical UUID.\n *\n * Returns null on anything that is not exactly 32 hex characters. Core Data\n * stores a UUID attribute as 16 raw bytes, so a different length means the\n * column is not what this code thinks it is — and a malformed ref that still\n * looks ref-shaped would be resolved against the wrong row rather than rejected.\n */\nconst toUuid = (hex: unknown): string | null => {\n if (hex === null || hex === undefined) return null;\n const h = String(hex).toLowerCase();\n if (!/^[0-9a-f]{32}$/.test(h)) return null;\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n};\n\n/** A `NULL AS alias` column reads back as null; every other value is coerced. */\nconst num = (v: unknown): number | null =>\n v === null || v === undefined ? null : Number(v as number);\nconst str = (v: unknown): string | null =>\n v === null || v === undefined ? null : String(v as string);\n\n/**\n * Pick the candidate column with the most non-null values.\n *\n * Returns null when none of the candidates exists, and also when they all exist\n * and are all empty — an all-null column is not a usable field, and reporting\n * one as resolved would produce a result whose every value is null with no\n * explanation.\n */\nconst resolveField = (\n db: DatabaseSync,\n table: string,\n columns: Set<string>,\n candidates: readonly string[],\n rows: number,\n): string | null => {\n const present = candidates.filter((c) => columns.has(c));\n if (present.length === 0) return null;\n if (rows === 0) return present[0] ?? null;\n let best: { name: string; count: number } | null = null;\n for (const name of present) {\n let count = 0;\n try {\n // COUNT() is always small. The VALUES are not — see PlaceRow.muid — so\n // nothing here ever selects the column itself.\n count = Number(\n (\n db.prepare(`SELECT COUNT(\"${name}\") AS c FROM \"${table}\"`).get() as {\n c: number | bigint;\n }\n ).c,\n );\n } catch {\n continue;\n }\n if (!best || count > best.count) best = { name, count };\n }\n return best && best.count > 0 ? best.name : (present[0] ?? null);\n};\n\n/**\n * The identifier column, but ONLY if it can actually carry every ref.\n *\n * Coverage is not enough here, and this is the difference between a fix and a\n * trap. A column populated on the rows Maps has written lately and null on the\n * ones that predate it would work perfectly for every place the user saves from\n * now on and fail silently for the places they already had — the failure mode\n * hardest to notice and worst to hit, since the older entries are the ones a\n * person is most likely to ask about.\n *\n * So the bar is TOTAL: set on every row, and distinct across every row. Anything\n * less returns null and the store falls back to `Z_PK`, which is worse but\n * uniformly worse. Measured on a real store: 24/24 favourites, 30/30 collection\n * items, 10/10 collections, 33/33 recents, all distinct.\n */\nconst resolveIdentifier = (\n db: DatabaseSync,\n table: string,\n columns: Set<string>,\n rows: number,\n): string | null => {\n const present = IDENTIFIER_CANDIDATES.filter((c) => columns.has(c));\n if (present.length === 0 || rows === 0) return null;\n for (const name of present) {\n try {\n const r = db\n .prepare(\n `SELECT COUNT(\"${name}\") AS nset, COUNT(DISTINCT \"${name}\") AS ndistinct FROM \"${table}\"`,\n )\n .get() as { nset: number | bigint; ndistinct: number | bigint };\n if (Number(r.nset) === rows && Number(r.ndistinct) === rows) return name;\n } catch {\n continue;\n }\n }\n return null;\n};\n\nconst countRows = (db: DatabaseSync, table: string): number => {\n try {\n return Number(\n (db.prepare(`SELECT COUNT(*) AS c FROM \"${table}\"`).get() as { c: number | bigint }).c,\n );\n } catch {\n return 0;\n }\n};\n\nconst describeEntity = (\n db: DatabaseSync,\n table: string,\n tables: string[],\n nameCandidates: readonly string[] = NAME_CANDIDATES,\n): EntityFacts => {\n const present = tables.includes(table);\n if (!present) {\n return {\n table,\n present: false,\n rows: 0,\n columns: new Set(),\n fields: {\n identifier: null,\n name: null,\n customName: null,\n latitude: null,\n longitude: null,\n address: null,\n muid: null,\n mapItem: null,\n created: null,\n modified: null,\n },\n };\n }\n const columns = new Set(columnsOf(db, table));\n const rows = countRows(db, table);\n const pick = (cands: readonly string[]): string | null =>\n resolveField(db, table, columns, cands, rows);\n return {\n table,\n present,\n rows,\n columns,\n fields: {\n identifier: resolveIdentifier(db, table, columns, rows),\n name: pick(nameCandidates),\n // Always named directly: it is the user's own label and must never be\n // confused with the place's name, even when it wins on coverage.\n customName: columns.has(\"ZCUSTOMNAME\") ? \"ZCUSTOMNAME\" : null,\n latitude: pick(LAT_CANDIDATES),\n longitude: pick(LON_CANDIDATES),\n address: pick(ADDRESS_CANDIDATES),\n muid: pick(MUID_CANDIDATES),\n mapItem: pick(MAP_ITEM_FK_CANDIDATES),\n created: pick(CREATED_CANDIDATES),\n modified: pick(MODIFIED_CANDIDATES),\n },\n };\n};\n\n/**\n * Maps' own places-per-guide, keyed by collection row id.\n *\n * Null when the store has no `ZPLACESCOUNT`, which changes what the resolver\n * below is allowed to accept — see `resolveMembership`.\n */\nconst declaredCounts = (db: DatabaseSync, collections: EntityFacts): Map<number, number> | null => {\n if (!collections.present || !collections.columns.has(COLLECTION_COUNT_COLUMN)) return null;\n try {\n const rows = db\n .prepare(`SELECT \"Z_PK\" AS pk, \"${COLLECTION_COUNT_COLUMN}\" AS n FROM \"${collections.table}\"`)\n .all() as { pk: number | bigint; n: number | bigint | null }[];\n return new Map(rows.map((r) => [Number(r.pk), Number(r.n ?? 0)]));\n } catch {\n return null;\n }\n};\n\n/**\n * Group a candidate's keys and count the items under each.\n *\n * Keys are read as TEXT because `ZMUID` is an INTEGER column holding 64-bit\n * place ids, and `node:sqlite` THROWS on those rather than truncating — reading\n * raw would drop the candidate into a catch instead of rejecting it on the\n * evidence. See `PlaceRow.muid`.\n */\nconst tally = (db: DatabaseSync, sql: string): Map<number, number> => {\n try {\n const rows = db.prepare(sql).all() as { pk: string | null; n: number | bigint }[];\n return new Map(\n rows.filter((r) => r.pk !== null).map((r) => [Number(r.pk), Number(r.n)] as const),\n );\n } catch {\n return new Map();\n }\n};\n\n/**\n * Does this candidate reproduce Maps' own counts, exactly, for every guide?\n *\n * Exactness is the whole test. `ZCOLLECTIONITEM.ZMAPITEM` matches 3 of 10\n * collections by coincidence, so \"mostly right\" is precisely the answer that\n * must be rejected. A key pointing at no collection at all (`unknown`) is\n * disqualifying for the same reason.\n */\nconst reproducesCounts = (declared: Map<number, number>, tallies: Map<number, number>): boolean => {\n for (const pk of tallies.keys()) if (!declared.has(pk)) return false;\n for (const [pk, n] of declared) if ((tallies.get(pk) ?? 0) !== n) return false;\n return true;\n};\n\n/**\n * Find how an item belongs to a collection, by running each candidate join.\n *\n * Scalar columns are tried before join tables only so the cheaper query runs\n * first; the verdict does not depend on order, because a candidate is accepted\n * only when it reproduces every count. When several would qualify the first is\n * taken, and that ambiguity cannot arise on a store whose counts are distinct.\n *\n * WITHOUT the oracle the bar changes rather than disappearing: a single join\n * table whose every key resolves on both sides is accepted, and anything\n * ambiguous is refused. That is weaker evidence, and it is the reason the\n * result is reported to callers rather than assumed.\n */\nconst resolveMembership = (\n db: DatabaseSync,\n collectionItems: EntityFacts,\n collections: EntityFacts,\n tables: string[],\n): CollectionMembership | null => {\n if (!collectionItems.present || !collections.present) return null;\n const declared = declaredCounts(db, collections);\n\n if (declared) {\n for (const column of collectionItems.columns) {\n if ([\"Z_PK\", \"Z_ENT\", \"Z_OPT\"].includes(column)) continue;\n const tallies = tally(\n db,\n `SELECT CAST(t.\"${column}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${collectionItems.table}\" t\n WHERE t.\"${column}\" IS NOT NULL GROUP BY t.\"${column}\"`,\n );\n if (tallies.size > 0 && reproducesCounts(declared, tallies))\n return { kind: \"column\", column };\n }\n }\n\n const joinTables = tables.filter((t) => t.startsWith(\"Z_\") && !RESERVED_JOIN_TABLES.has(t));\n const accepted: CollectionMembership[] = [];\n for (const table of joinTables) {\n const columns = columnsOf(db, table);\n // Both orientations: the column names do not say which side is which, and\n // reading `Z_6COLLECTIONS` as the collection side is an inference, not a\n // fact. On the real store the wrong orientation scores 3 of 10.\n for (const collectionColumn of columns) {\n for (const itemColumn of columns) {\n if (collectionColumn === itemColumn) continue;\n const tallies = tally(\n db,\n `SELECT CAST(j.\"${collectionColumn}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${table}\" j\n JOIN \"${collectionItems.table}\" t ON t.\"Z_PK\" = j.\"${itemColumn}\"\n GROUP BY j.\"${collectionColumn}\"`,\n );\n if (tallies.size === 0) continue;\n if (declared) {\n if (reproducesCounts(declared, tallies))\n return { kind: \"joinTable\", table, collectionColumn, itemColumn };\n continue;\n }\n // No oracle: demand that every key on both sides resolves to a real row.\n const orphaned = tally(\n db,\n `SELECT CAST(j.\"${collectionColumn}\" AS TEXT) AS pk, COUNT(*) AS n FROM \"${table}\" j\n LEFT JOIN \"${collections.table}\" c ON c.\"Z_PK\" = j.\"${collectionColumn}\"\n WHERE c.\"Z_PK\" IS NULL GROUP BY j.\"${collectionColumn}\"`,\n );\n if (orphaned.size === 0)\n accepted.push({ kind: \"joinTable\", table, collectionColumn, itemColumn });\n }\n }\n }\n // Exactly one unambiguous reading, or nothing. Two candidates mean the\n // evidence does not distinguish them, and picking one would be a guess.\n return accepted.length === 1 ? (accepted[0] ?? null) : null;\n};\n\n/** The mechanism as one short string, for diagnostics. */\nconst describeMembership = (m: CollectionMembership | null): string | null => {\n if (!m) return null;\n return m.kind === \"column\" ? m.column : `${m.table}(${m.collectionColumn}, ${m.itemColumn})`;\n};\n\nexport const introspect = (db: DatabaseSync, now: number = Date.now()): StoreCapabilities => {\n const tables = (\n db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`).all() as {\n name: string;\n }[]\n )\n .map((r) => r.name)\n .filter(isRealTable);\n\n const favorites = describeEntity(db, FAVORITES_TABLE, tables);\n const collectionItems = describeEntity(db, COLLECTION_ITEMS_TABLE, tables);\n const history = describeEntity(db, HISTORY_TABLE, tables);\n\n if (!favorites.present && !collectionItems.present && !history.present) {\n throw new SchemaDriftError(\n `Maps' store has none of \"${FAVORITES_TABLE}\", \"${COLLECTION_ITEMS_TABLE}\" or ` +\n `\"${HISTORY_TABLE}\" (found: ${tables.join(\", \") || \"nothing\"}). This is either not a ` +\n `MapsSync store or Apple has restructured it. Nothing can be read from it.`,\n );\n }\n\n const collections = describeEntity(db, COLLECTIONS_TABLE, tables, COLLECTION_TITLE_CANDIDATES);\n const mapItems = describeEntity(db, MAP_ITEMS_TABLE, tables);\n\n const membership = resolveMembership(db, collectionItems, collections, tables);\n\n // The epoch comes from whichever place table has the most rows and a created\n // column — one measurement for the whole store, since every entity is written\n // by the same framework.\n const dated = [favorites, collectionItems, history, mapItems]\n .filter((e) => e.present && e.rows > 0 && e.fields.created)\n .toSorted((a, b) => b.rows - a.rows)[0];\n let maxCreated: number | null = null;\n if (dated?.fields.created) {\n try {\n maxCreated = (\n db\n .prepare(`SELECT MAX(CAST(\"${dated.fields.created}\" AS REAL)) AS m FROM \"${dated.table}\"`)\n .get() as { m: number | null }\n ).m;\n } catch {\n maxCreated = null;\n }\n }\n\n return {\n fingerprint: fingerprintSchema(db),\n tables,\n favorites,\n collections,\n collectionItems,\n history,\n mapItems,\n membership,\n collectionFk: describeMembership(membership),\n epoch: resolveEpoch(maxCreated, now),\n };\n};\n\nexport class MapsStore {\n readonly db: DatabaseSync;\n readonly caps: StoreCapabilities;\n readonly path: string;\n readonly mode: ReadOnlyMode;\n\n constructor(opts: {\n db: DatabaseSync;\n caps: StoreCapabilities;\n path: string;\n mode: ReadOnlyMode;\n }) {\n this.db = opts.db;\n this.caps = opts.caps;\n this.path = opts.path;\n this.mode = opts.mode;\n }\n\n /**\n * One column, or `NULL` under the same alias when it could not be resolved.\n *\n * The same guard `packages/safari` uses, and needed more here: this schema was\n * read from a probe report rather than from the database, so a wrong\n * expectation should cost one field instead of the whole lane.\n */\n #col(column: string | null, alias: string, table = \"t\"): string {\n return column ? `${table}.\"${column}\" AS ${alias}` : `NULL AS ${alias}`;\n }\n\n /**\n * The same, for a column whose value may not fit in a JS double.\n *\n * SQLite holds 64-bit integers; a JS number holds 53 bits of them, and\n * `node:sqlite` refuses rather than silently losing precision. Casting in SQL\n * keeps the exact value and moves the decision about what to do with it out\n * of the driver.\n */\n #colText(column: string | null, alias: string, table = \"t\"): string {\n return column ? `CAST(${table}.\"${column}\" AS TEXT) AS ${alias}` : `NULL AS ${alias}`;\n }\n\n #placeSelect(e: EntityFacts): string {\n const f = e.fields;\n return [\n `t.\"Z_PK\" AS id`,\n f.identifier ? `HEX(t.\"${f.identifier}\") AS uuid` : `NULL AS uuid`,\n this.#col(f.name, \"name\"),\n this.#col(f.customName, \"customName\"),\n this.#col(f.latitude, \"latitude\"),\n this.#col(f.longitude, \"longitude\"),\n this.#col(f.address, \"address\"),\n this.#colText(f.muid, \"muid\"),\n this.#col(f.created, \"createdRaw\"),\n this.#col(f.modified, \"modifiedRaw\"),\n f.mapItem ? `t.\"${f.mapItem}\" AS mapItem` : `NULL AS mapItem`,\n ].join(\", \");\n }\n\n #toPlace(r: Record<string, unknown>): PlaceRow {\n return {\n id: Number(r.id),\n uuid: toUuid(r.uuid),\n name: str(r.name),\n customName: str(r.customName),\n latitude: num(r.latitude),\n longitude: num(r.longitude),\n address: str(r.address),\n // MEASURED on a real store: 12 of 20 linked favourites carry ZMUID = 0.\n // Zero is a sentinel for \"no Apple place id\", not an id, and reporting it\n // as one invites a caller to treat two unrelated places as the same\n // place. Only 8 of the 20 have a real id, and all 8 are 19 digits.\n muid:\n r.muid === null || r.muid === undefined || String(r.muid) === \"0\" ? null : String(r.muid),\n createdRaw: num(r.createdRaw),\n modifiedRaw: num(r.modifiedRaw),\n linked: r.mapItem !== null && r.mapItem !== undefined,\n };\n }\n\n #entityFor(kind: \"favorite\" | \"collection-item\" | \"history\"): EntityFacts {\n if (kind === \"favorite\") return this.caps.favorites;\n if (kind === \"collection-item\") return this.caps.collectionItems;\n return this.caps.history;\n }\n\n /**\n * Items belonging to no collection at all, or null when unanswerable.\n *\n * MEASURED: 30 collection items, 18 filed in a guide, 12 in none — and Core\n * Data has deleted nothing (`Z_PRIMARYKEY.Z_MAX` equals the live count for\n * both `Collection` and `CollectionItem`), so these are not the debris of\n * removed guides. 7 of the 12 appear nowhere else in the store: not as a\n * favourite, not in another guide, not in recents. They are places the user\n * saved that no other tool can reach.\n *\n * `NOT IN` needs the null guard. A subquery yielding a single NULL makes\n * `NOT IN` false for EVERY row, so the filter would silently return nothing\n * and read as \"you have no unfiled places\" — the failure this whole surface\n * keeps having to design against.\n */\n #unfiledClause(): string | null {\n const m = this.caps.membership;\n if (!m) return null;\n if (m.kind === \"column\") return `t.\"${m.column}\" IS NULL`;\n return (\n `t.\"Z_PK\" NOT IN (SELECT j.\"${m.itemColumn}\" FROM \"${m.table}\" j ` +\n `WHERE j.\"${m.itemColumn}\" IS NOT NULL)`\n );\n }\n\n /** How many collection items are filed in no collection; null when unknown. */\n unfiledCount(): number | null {\n const e = this.caps.collectionItems;\n const clause = this.#unfiledClause();\n if (!e.present || !clause) return null;\n try {\n return Number(\n (\n this.db.prepare(`SELECT COUNT(*) AS c FROM \"${e.table}\" t WHERE ${clause}`).get() as {\n c: number | bigint;\n }\n ).c,\n );\n } catch {\n return null;\n }\n }\n\n /** One entity's places, newest first when a date is available. */\n places(\n kind: \"favorite\" | \"collection-item\" | \"history\",\n opts: {\n limit: number;\n collectionId?: number | undefined;\n query?: string | undefined;\n /** Only items filed in NO collection. See `#unfiledClause`. */\n unfiled?: boolean | undefined;\n },\n ): { rows: PlaceRow[]; truncated: boolean } {\n const e = this.#entityFor(kind);\n if (!e.present || e.rows === 0) return { rows: [], truncated: false };\n\n const where: string[] = [];\n const params: (string | number)[] = [];\n\n if (opts.collectionId !== undefined) {\n const m = this.caps.membership;\n // Nothing reproduced ZPLACESCOUNT, so which places are in this guide is\n // genuinely unknown. Empty is the honest answer and callers are told why.\n if (!m) return { rows: [], truncated: false };\n if (m.kind === \"column\") {\n where.push(`t.\"${m.column}\" = ?`);\n } else {\n // IN, not JOIN, because the question is set membership: is this item\n // in this guide. A JOIN also multiplies the row by the number of\n // matching join rows, which is 1 today only because Core Data gives the\n // table a compound primary key — a property of the schema rather than\n // of the query. IN does not depend on it.\n where.push(\n `t.\"Z_PK\" IN (SELECT j.\"${m.itemColumn}\" FROM \"${m.table}\" j ` +\n `WHERE j.\"${m.collectionColumn}\" = ?)`,\n );\n }\n params.push(opts.collectionId);\n }\n\n if (opts.unfiled) {\n const clause = this.#unfiledClause();\n // Membership is unresolved, so \"in no collection\" is not a question this\n // store can answer either. Empty, and the caller is told why.\n if (!clause) return { rows: [], truncated: false };\n where.push(clause);\n }\n\n if (opts.query) {\n const pattern = `%${escapeLike(opts.query)}%`;\n const clauses: string[] = [];\n for (const col of [e.fields.name, e.fields.customName, e.fields.address]) {\n if (!col) continue;\n clauses.push(`t.\"${col}\" LIKE ? ESCAPE '\\\\'`);\n params.push(pattern);\n }\n // No searchable column means no rows can match. Returning everything\n // would be worse than returning nothing: it reads as a successful search.\n if (clauses.length === 0) return { rows: [], truncated: false };\n where.push(`(${clauses.join(\" OR \")})`);\n }\n\n const limit = Math.max(1, opts.limit);\n params.push(limit + 1);\n\n const order = e.fields.modified\n ? `t.\"${e.fields.modified}\" DESC`\n : e.fields.created\n ? `t.\"${e.fields.created}\" DESC`\n : `t.\"Z_PK\" DESC`;\n\n const sql =\n `SELECT ${this.#placeSelect(e)} FROM \"${e.table}\" t ` +\n (where.length ? `WHERE ${where.join(\" AND \")} ` : \"\") +\n `ORDER BY ${order} LIMIT ?`;\n\n const raw = this.db.prepare(sql).all(...params) as Record<string, unknown>[];\n return {\n rows: raw.slice(0, limit).map((r) => this.#toPlace(r)),\n truncated: raw.length > limit,\n };\n }\n\n /**\n * The WHERE clause and parameter for one key, or null when the key cannot be\n * honoured against this entity.\n *\n * A uuid key against a store with no resolved identifier column returns null\n * rather than falling back to the row id. The two number spaces are unrelated,\n * so a fallback would resolve to a real but WRONG place — the one outcome\n * worse than not finding it.\n *\n * `HEX()` on both sides rather than `X'..'` literal: it keeps the comparison\n * in one form, and these tables are tens of rows, so the lost index is free.\n */\n #keyClause(e: EntityFacts, key: EntityKey): { sql: string; param: string | number } | null {\n if (\"uuid\" in key) {\n if (!e.fields.identifier) return null;\n return {\n sql: `HEX(t.\"${e.fields.identifier}\") = ?`,\n param: key.uuid.replaceAll(\"-\", \"\").toUpperCase(),\n };\n }\n return { sql: `t.\"Z_PK\" = ?`, param: key.rowId };\n }\n\n /** One place by entity and key. */\n place(kind: \"favorite\" | \"collection-item\" | \"history\", key: EntityKey): PlaceRow | null {\n const e = this.#entityFor(kind);\n if (!e.present) return null;\n const clause = this.#keyClause(e, key);\n if (!clause) return null;\n const sql = `SELECT ${this.#placeSelect(e)} FROM \"${e.table}\" t WHERE ${clause.sql} LIMIT 1`;\n const r = this.db.prepare(sql).get(clause.param) as Record<string, unknown> | undefined;\n return r ? this.#toPlace(r) : null;\n }\n\n /**\n * A collection key to the row id its items point at.\n *\n * Collection membership is a Core Data foreign key, so it holds `Z_PK` values\n * whatever the ref carries. A uuid ref has to be translated before it can\n * filter items, and this is the one place that happens.\n */\n collectionRowId(key: EntityKey): number | null {\n if (!(\"uuid\" in key)) return key.rowId;\n const e = this.caps.collections;\n const clause = this.#keyClause(e, key);\n if (!e.present || !clause) return null;\n const r = this.db\n .prepare(`SELECT t.\"Z_PK\" AS id FROM \"${e.table}\" t WHERE ${clause.sql} LIMIT 1`)\n .get(clause.param) as { id: number | bigint } | undefined;\n return r ? Number(r.id) : null;\n }\n\n collections(opts: { limit: number }): { rows: CollectionRow[]; truncated: boolean } {\n const e = this.caps.collections;\n if (!e.present || e.rows === 0) return { rows: [], truncated: false };\n const limit = Math.max(1, opts.limit);\n const hasCount = e.columns.has(\"ZPLACESCOUNT\");\n const order = e.fields.modified ? `t.\"${e.fields.modified}\" DESC` : `t.\"Z_PK\" DESC`;\n const sql =\n `SELECT t.\"Z_PK\" AS id, ` +\n `${e.fields.identifier ? `HEX(t.\"${e.fields.identifier}\") AS uuid` : `NULL AS uuid`}, ` +\n `${this.#col(e.fields.name, \"title\")}, ` +\n `${hasCount ? `t.\"ZPLACESCOUNT\" AS placesCount` : `NULL AS placesCount`}, ` +\n `${this.#col(e.fields.created, \"createdRaw\")}, ` +\n `${this.#col(e.fields.modified, \"modifiedRaw\")} ` +\n `FROM \"${e.table}\" t ORDER BY ${order} LIMIT ?`;\n const raw = this.db.prepare(sql).all(limit + 1) as Record<string, unknown>[];\n return {\n rows: raw.slice(0, limit).map((r) => ({\n id: Number(r.id),\n uuid: toUuid(r.uuid),\n title: r.title === null || r.title === undefined ? null : String(r.title),\n placesCount: r.placesCount === null ? null : Number(r.placesCount),\n createdRaw: r.createdRaw === null ? null : Number(r.createdRaw),\n modifiedRaw: r.modifiedRaw === null ? null : Number(r.modifiedRaw),\n })),\n truncated: raw.length > limit,\n };\n }\n\n close(): void {\n try {\n this.db.close();\n } catch {\n // Already closed, or the handle died with the process. Nothing to do.\n }\n }\n}\n\nexport const openStore = (opts: {\n path: string;\n mode?: ReadOnlyMode;\n logger?: Logger;\n now?: number;\n hint?: string;\n}): MapsStore => {\n const opened = openReadOnly(opts.path, opts.mode ?? \"auto\", {\n envVar: \"APPLE_MAPS_INDEX_MODE\",\n label: \"Maps' place store\",\n ...(opts.hint ? { hint: opts.hint } : {}),\n validate: (db) => introspect(db, opts.now),\n fatal: (err) => err instanceof SchemaDriftError,\n onFallback: () =>\n opts.logger?.warn?.(\n \"maps: opened immutable; places saved since the last checkpoint may be missing\",\n ),\n });\n return new MapsStore({\n db: opened.db,\n caps: opened.validated,\n path: opts.path,\n mode: opened.mode,\n });\n};\n","import { execFileSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { DatabaseSync } from \"node:sqlite\";\n\nimport { AppleAutomationError } from \"@mgcrea/mcp-apple-core\";\n\nimport { CORE_DATA_EPOCH_OFFSET } from \"./dates.js\";\nimport type { EntityKey } from \"./store.js\";\n\n/**\n * Writing to Maps' store.\n *\n * ## Why this is SQL and not the app's own API\n *\n * Maps ships no scripting dictionary and registers no App Intents on macOS, so\n * there is no lane where the app performs the write on our behalf. Every other\n * surface in this repo writes through an Apple Event; this one cannot, and the\n * alternatives were measured rather than assumed — see `docs/maps.md`.\n *\n * ## The one thing that cannot be synthesised, and how it is obtained anyway\n *\n * A place is only real to Maps if it has a `ZMAPITEMSTORAGE` — a GEO protobuf of\n * one to four kilobytes that this repo has never decoded and cannot generate.\n *\n * It does not have to. **Opening a place makes Maps write one.**\n * `maps://?q=<name>&ll=<lat>,<lon>` goes through LaunchServices — not Apple\n * Events, not Accessibility — and Maps resolves the place and records it in\n * Recents with a full record attached. That record is then copied into the new\n * favourite. The blob is always minted by Maps, which is the only thing that can\n * mint one correctly.\n *\n * MEASURED consequences of that design, all of them in `docs/maps.md`:\n *\n * * The insert needs THREE tables — the favourite, its map item, and the two\n * `Z_PRIMARYKEY` counters. Not the eight the app touches.\n * * No persistent history and no `NSCK*` metadata is written. Core Data\n * reconciles unregistered objects on the app's next save and mirrors them to\n * iCloud by itself; a favourite written this way reached another device.\n * * It works with Maps RUNNING, and survives a subsequent save by the app.\n * There is no need to quit anything.\n *\n * ## The cost the caller must be told about\n *\n * Seeding leaves the place in the user's **Recents**, whether or not the\n * favourite is kept. That is unavoidable — it is the mechanism — so every tool\n * built on this says so in its description rather than springing it on somebody.\n *\n * ## Why writes are dangerous here in a way the other surfaces are not\n *\n * The store is mirrored by `NSPersistentCloudKitContainer`, and mirroring does\n * not wait to be told. A malformed row is not a local mistake: it reaches every\n * device on the account as soon as Maps next runs. There is no such thing as a\n * local-only insert here — that was believed for a while and measured false.\n * Hence: never fabricate a place record, only ever copy one Maps wrote.\n */\n\nconst STORAGE_POLL_MS = 250;\n\n/**\n * Progress, on stderr.\n *\n * Adding a favourite can take tens of seconds — Maps has to resolve a place over\n * the network before there is anything to copy — and the first version said\n * NOTHING for the whole of it. That is indistinguishable from a hang, and was\n * reported as one three times while three different theories were tried. A long\n * operation that reports nothing cannot be diagnosed, only guessed at.\n *\n * stderr because this is an MCP stdio server: stdout carries the protocol, and\n * the server already writes its banner here.\n */\nconst progress = (message: string): void => {\n process.stderr.write(`[apple-maps-mcp] ${message}\\n`);\n};\n\nexport class MapsWriteError extends AppleAutomationError {\n override readonly name = \"MapsWriteError\";\n}\n\n/** Injected so tests never launch an application. */\nexport type OpenUrl = (url: string) => void;\n\nexport const defaultOpenUrl: OpenUrl = (url) => {\n // `-g` so the user's foreground app is not stolen. `timeout` because a wedged\n // LaunchServices would otherwise block forever, and this runs inside a tool\n // call somebody is waiting on.\n execFileSync(\"/usr/bin/open\", [\"-g\", url], { timeout: 10_000, stdio: \"ignore\" });\n};\n\nexport type AddFavoriteInput = {\n /** What to search for. A name; a bare coordinate does NOT open a place card. */\n query: string;\n latitude?: number | undefined;\n longitude?: number | undefined;\n /** The label to store. Defaults to `query`. */\n name?: string | undefined;\n};\n\nexport type AddFavoriteResult = {\n rowId: number;\n uuid: string;\n name: string | null;\n latitude: number | null;\n longitude: number | null;\n /** False when an equivalent favourite already existed and was returned as-is. */\n created: boolean;\n /** True when Maps had to be asked to resolve the place, leaving a Recents entry. */\n seeded: boolean;\n};\n\n/** The columns needed to return an existing favourite without re-reading it. */\ntype ExistingFavorite = {\n pk: number;\n hex: string;\n name: string | null;\n lat: number;\n lon: number;\n};\n\ntype Donor = {\n mapPk: number;\n lat: number;\n lon: number;\n name: string | null;\n address: string | null;\n category: string | null;\n muid: string | null;\n storage: Uint8Array;\n};\n\nconst uuidBytes = (): Uint8Array =>\n Uint8Array.from(Buffer.from(randomUUID().replaceAll(\"-\", \"\"), \"hex\"));\n\nconst uuidOf = (bytes: Uint8Array): string => {\n const h = Buffer.from(bytes).toString(\"hex\");\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n};\n\n/**\n * Coordinates are floats and never compare exactly across sources, so identity\n * is \"within about a metre\" rather than equality. 1e-5 degrees is ~1.1 m of\n * latitude — close enough that two rows are the same doorway, far enough apart\n * that neighbouring shops are not merged.\n */\nconst NEAR = 1e-5;\n\n/** Same doorway, within about a metre. */\nconst near = (aLat: number, aLon: number, bLat: number, bLon: number): boolean =>\n Math.abs(aLat - bLat) <= SEED_TOLERANCE && Math.abs(aLon - bLon) <= SEED_TOLERANCE;\n\n/**\n * How far a seeded place may sit from the coordinate the caller gave.\n *\n * Far looser than `NEAR`: the caller's coordinate and Apple's for the same place\n * routinely differ by a building's width, and a search resolves to the entrance\n * rather than the pin. ~250 m accepts that and still rejects a different town.\n */\nconst SEED_TOLERANCE = 2.5e-3;\n\nconst toRad = (deg: number): number => (deg * Math.PI) / 180;\n\n/** Metres between two coordinates, for the error message only. */\nconst haversineMetres = (aLat: number, aLon: number, bLat: number, bLon: number): number => {\n const dLat = toRad(bLat - aLat);\n const dLon = toRad(bLon - aLon);\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(aLat)) * Math.cos(toRad(bLat)) * Math.sin(dLon / 2) ** 2;\n return 6_371_000 * 2 * Math.asin(Math.sqrt(h));\n};\n\nexport class MapsWriter {\n readonly #path: string;\n readonly #openUrl: OpenUrl;\n readonly #seedTimeoutMs: number;\n\n constructor(opts: { storePath: string; openUrl?: OpenUrl; seedTimeoutMs?: number }) {\n this.#path = opts.storePath;\n this.#openUrl = opts.openUrl ?? defaultOpenUrl;\n this.#seedTimeoutMs = opts.seedTimeoutMs ?? 30_000;\n }\n\n /**\n * A read-write handle.\n *\n * `busy_timeout` is not optional: Maps and `mapssyncd` hold this store open,\n * and without it a write during ordinary use is refused outright rather than\n * waiting for the lock the way any other SQLite client would.\n */\n #open(): DatabaseSync {\n const db = new DatabaseSync(this.#path);\n db.exec(\"PRAGMA busy_timeout = 5000\");\n return db;\n }\n\n #entity(db: DatabaseSync, name: string): number {\n const row = db.prepare(`SELECT Z_ENT AS e FROM Z_PRIMARYKEY WHERE Z_NAME = ?`).get(name) as\n | { e: number | bigint }\n | undefined;\n if (!row)\n throw new MapsWriteError(`This store has no \"${name}\" entity, so it cannot be written.`);\n return Number(row.e);\n }\n\n /**\n * The next primary key: one past whichever is HIGHER, the counter or the\n * highest row actually present.\n *\n * `Z_PRIMARYKEY.Z_MAX` is the counter Core Data maintains, and trusting it\n * alone is what a first version did. It is not always current — Maps can be\n * mid-write, a crash can leave it behind, and a foreign writer that forgot to\n * bump it (a mistake this very file guards against making) leaves it stale\n * forever. Taking `MAX(Z_PK)` as well costs one indexed lookup and makes a\n * collision impossible rather than unlikely.\n *\n * Found by a test whose stand-in for Maps inserted a row without bumping the\n * counter. The stand-in was unfaithful; the bug it exposed was not.\n */\n #nextPk(db: DatabaseSync, name: string, table: string): number {\n const counter = db.prepare(`SELECT Z_MAX AS m FROM Z_PRIMARYKEY WHERE Z_NAME = ?`).get(name) as\n | { m: number | bigint }\n | undefined;\n const highest = db.prepare(`SELECT MAX(Z_PK) AS m FROM \"${table}\"`).get() as\n | { m: number | bigint | null }\n | undefined;\n return Math.max(Number(counter?.m ?? 0), Number(highest?.m ?? 0)) + 1;\n }\n\n /**\n * An existing favourite for this place.\n *\n * Two ways to match, because a coordinate alone is not enough in either\n * direction. TIGHT coordinate (~1 m) catches the canonical case, where both\n * numbers came from Maps. NAME plus a LOOSE coordinate (~250 m) catches the\n * case that matters to a caller: it passed its own coordinate, which differs\n * from Apple's by a building's width — measured at 6.9e-5 for Sagrada Família.\n *\n * Without the second, every repeated call re-seeded before discovering the\n * duplicate, leaving a Recents entry each time for a favourite it then did not\n * create. Loosening the coordinate ALONE was the wrong fix: two different shops\n * 200 m apart would merge. Requiring the name as well makes the loose radius\n * safe, because it only ever merges rows the caller already calls the same\n * thing.\n */\n #existingFavorite(db: DatabaseSync, lat: number, lon: number, name?: string) {\n const tight = db\n .prepare(\n `SELECT Z_PK AS pk, HEX(ZIDENTIFIER) AS hex, ZMAPITEMNAME AS name,\n ZLATITUDE AS lat, ZLONGITUDE AS lon\n FROM ZFAVORITEITEM\n WHERE ZLATITUDE BETWEEN ? AND ? AND ZLONGITUDE BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(lat - NEAR, lat + NEAR, lon - NEAR, lon + NEAR) as ExistingFavorite | undefined;\n if (tight || !name) return tight;\n\n return db\n .prepare(\n `SELECT Z_PK AS pk, HEX(ZIDENTIFIER) AS hex, ZMAPITEMNAME AS name,\n ZLATITUDE AS lat, ZLONGITUDE AS lon\n FROM ZFAVORITEITEM\n WHERE ZMAPITEMNAME = ?\n AND ZLATITUDE BETWEEN ? AND ? AND ZLONGITUDE BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(\n name,\n lat - SEED_TOLERANCE,\n lat + SEED_TOLERANCE,\n lon - SEED_TOLERANCE,\n lon + SEED_TOLERANCE,\n ) as ExistingFavorite | undefined;\n }\n\n /**\n * A place record already in the store for this coordinate.\n *\n * Checked before seeding, so asking for somewhere the user has already looked\n * at costs no Recents entry and no network round trip. Recents, collection\n * items and other favourites are all valid sources — the record is the same\n * object whichever row points at it.\n */\n #donorNear(db: DatabaseSync, lat: number, lon: number): Donor | null {\n const row = db\n .prepare(\n `SELECT m.\"Z_PK\" AS mapPk, m.\"ZLATITUDE\" AS lat, m.\"ZLONGITUDE\" AS lon,\n m.\"ZMAPITEMSTORAGE\" AS storage,\n COALESCE(f.\"ZMAPITEMNAME\", ci.\"ZMAPITEMNAME\") AS name,\n COALESCE(f.\"ZMAPITEMADDRESS\", ci.\"ZMAPITEMADDRESS\") AS address,\n COALESCE(f.\"ZMAPITEMCATEGORY\", ci.\"ZMAPITEMCATEGORY\") AS category,\n CAST(COALESCE(f.\"ZMUID\", ci.\"ZMUID\") AS TEXT) AS muid\n FROM \"ZMIXINMAPITEM\" m\n LEFT JOIN \"ZFAVORITEITEM\" f ON f.\"ZMAPITEM\" = m.\"Z_PK\"\n LEFT JOIN \"ZCOLLECTIONITEM\" ci ON ci.\"ZMAPITEM\" = m.\"Z_PK\"\n WHERE m.\"ZMAPITEMSTORAGE\" IS NOT NULL\n AND m.\"ZLATITUDE\" BETWEEN ? AND ? AND m.\"ZLONGITUDE\" BETWEEN ? AND ?\n LIMIT 1`,\n )\n .get(lat - NEAR, lat + NEAR, lon - NEAR, lon + NEAR) as Donor | undefined;\n return row ?? null;\n }\n\n /** The newest recent carrying a place record, above a watermark. */\n #seededDonor(db: DatabaseSync, sinceHistoryPk: number): Donor | null {\n const row = db\n .prepare(\n `SELECT m.\"Z_PK\" AS mapPk, m.\"ZLATITUDE\" AS lat, m.\"ZLONGITUDE\" AS lon,\n m.\"ZMAPITEMSTORAGE\" AS storage,\n h.\"ZCUSTOMNAME\" AS name, NULL AS address, NULL AS category,\n CAST(h.\"ZMUID\" AS TEXT) AS muid\n FROM \"ZHISTORYITEM\" h\n JOIN \"ZMIXINMAPITEM\" m ON m.\"Z_PK\" = h.\"ZMAPITEM\"\n WHERE h.\"Z_PK\" > ? AND m.\"ZMAPITEMSTORAGE\" IS NOT NULL\n ORDER BY h.\"Z_PK\" DESC LIMIT 1`,\n )\n .get(sinceHistoryPk) as Donor | undefined;\n return row ?? null;\n }\n\n #maxHistory(db: DatabaseSync): number {\n const row = db.prepare(`SELECT MAX(Z_PK) AS m FROM ZHISTORYITEM`).get() as\n | { m: number | bigint | null }\n | undefined;\n return Number(row?.m ?? 0);\n }\n\n /**\n * Add a favourite, in three phases with a connection open for as little of it\n * as possible.\n *\n * THE PHASES ARE THE POINT. A first version opened one read-write handle at the\n * top and held it across the whole seed — including the wait for Maps to\n * resolve a place, which can run to tens of seconds. That hung: Maps is being\n * asked to WRITE the very record being waited for, into a store this process is\n * holding open for writing. Whatever the precise interaction, the shape was a\n * departure from the sequence that had been proven by hand, and the proven\n * sequence never holds a write handle while waiting on the app.\n *\n * 1. READ — look for an existing favourite and an existing place record.\n * 2. SEED — ask Maps to mint a record, holding NO connection at all, and\n * poll with a short-lived read-only handle each time.\n * 3. WRITE — open read-write, insert, close.\n */\n addFavorite(input: AddFavoriteInput): AddFavoriteResult {\n const hasCoords = input.latitude !== undefined && input.longitude !== undefined;\n const lat = input.latitude ?? 0;\n const lon = input.longitude ?? 0;\n\n // ── 1. read ──────────────────────────────────────────────────────────────\n progress(`add_favorite ${JSON.stringify(input.query)}: reading the store`);\n const ro = new DatabaseSync(this.#path, { readOnly: true });\n let existing: {\n pk: number;\n hex: string;\n name: string | null;\n lat: number;\n lon: number;\n } | null = null;\n let donor: Donor | null = null;\n let watermark = 0;\n try {\n if (hasCoords) {\n existing = this.#existingFavorite(ro, lat, lon, input.name ?? input.query) ?? null;\n donor = this.#donorNear(ro, lat, lon);\n }\n watermark = this.#maxHistory(ro);\n } finally {\n ro.close();\n }\n\n if (existing) {\n // Idempotent on purpose. A model that retries must not produce two\n // favourites for one place — and Maps HIDES the duplicate in its own UI,\n // so the store would disagree with the app invisibly.\n return {\n rowId: Number(existing.pk),\n uuid: uuidOf(Buffer.from(existing.hex, \"hex\")),\n name: existing.name,\n latitude: Number(existing.lat),\n longitude: Number(existing.lon),\n created: false,\n seeded: false,\n };\n }\n\n // ── 2. seed, holding nothing ─────────────────────────────────────────────\n let seeded = false;\n if (!donor) {\n /*\n * THE SEED URL CARRIES NO COORDINATE, and that is measured rather than\n * stylistic.\n *\n * `maps://?q=<name>` is the form proved to make Maps file the place in\n * Recents with a record attached. An earlier version appended\n * `&ll=<lat>,<lon>` on the strength of an Accessibility measurement where\n * that form opened a place CARD — but a card is not a Recents entry, and\n * with the coordinate present Maps appears to treat the URL as \"show this\n * location\" rather than a search worth recording. Nothing was ever written,\n * so the poll ran to its timeout every time.\n *\n * The coordinate is still used, for de-duplication and for checking that\n * what Maps resolved is the place that was asked for. It just does not go\n * into the URL.\n */\n const url = `maps://?q=${encodeURIComponent(input.query)}`;\n progress(`asking Maps to resolve ${JSON.stringify(input.query)}…`);\n this.#openUrl(url);\n seeded = true;\n progress(`opened, waiting up to ${Math.round(this.#seedTimeoutMs / 1000)}s for a record`);\n const deadline = Date.now() + this.#seedTimeoutMs;\n let polls = 0;\n while (Date.now() < deadline && !donor) {\n // A fresh handle per poll: under WAL a connection that has begun reading\n // holds a snapshot and may not see another process's commits.\n const probe = new DatabaseSync(this.#path, { readOnly: true });\n try {\n donor = this.#seededDonor(probe, watermark);\n } finally {\n probe.close();\n }\n if (!donor) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, STORAGE_POLL_MS);\n polls += 1;\n if (polls % 8 === 0) progress(`still waiting (${polls * (STORAGE_POLL_MS / 1000)}s)…`);\n }\n if (donor && hasCoords && !near(donor.lat, donor.lon, lat, lon)) {\n // Maps resolved SOMETHING, but not the place that was asked for. Copying\n // its record would make a favourite for the wrong place — which then\n // syncs to every device on the account — so this refuses rather than\n // guesses.\n throw new MapsWriteError(\n `Maps resolved ${JSON.stringify(input.query)} to a place about ` +\n `${Math.round(haversineMetres(donor.lat, donor.lon, lat, lon))}m from the coordinate ` +\n `given, so it is probably not the place meant. Nothing was saved — try a more ` +\n `specific name.`,\n { query: input.query },\n );\n }\n if (donor)\n progress(`got a place record (${donor.storage?.length ?? 0} bytes) after ${polls} polls`);\n if (!donor) {\n throw new MapsWriteError(\n `Maps did not produce a place record for \"${input.query}\" within ` +\n `${Math.round(this.#seedTimeoutMs / 1000)}s. Maps must be RUNNING for this to work — ` +\n `it is the only thing that can create a place record. It may also have resolved the ` +\n `query to a search rather than a place, since searches get no record. Try a more ` +\n `specific name, or pass latitude and longitude.`,\n { query: input.query },\n );\n }\n }\n\n /*\n * DE-DUPLICATE AGAIN, against the RESOLVED coordinate.\n *\n * The check in phase 1 uses the coordinate the CALLER supplied, and that is\n * the wrong kind of number to compare with. MEASURED: asked to save Sagrada\n * Família at longitude 2.1744, Maps resolved and stored 2.1743308 — 6.9e-5\n * away, seven times the ~1 m tolerance. So a caller repeating its own request\n * verbatim did not match the row its first call had created, and a second\n * favourite appeared.\n *\n * Maps' own coordinate is canonical: whatever it resolved this time, it\n * resolved last time too. Comparing that against what is stored is comparing\n * like with like, and it is exact enough for the tight tolerance.\n *\n * The phase-1 check still earns its place — when it hits, no seeding happens\n * and no Recents entry appears. This one is what makes the tool correct.\n */\n const canonical = new DatabaseSync(this.#path, { readOnly: true });\n let duplicate: ExistingFavorite | null = null;\n try {\n duplicate = this.#existingFavorite(canonical, donor.lat, donor.lon) ?? null;\n } finally {\n canonical.close();\n }\n if (duplicate) {\n progress(\"already a favourite; nothing written\");\n return {\n rowId: Number(duplicate.pk),\n uuid: uuidOf(Buffer.from(duplicate.hex, \"hex\")),\n name: duplicate.name,\n latitude: Number(duplicate.lat),\n longitude: Number(duplicate.lon),\n created: false,\n seeded,\n };\n }\n\n // ── 3. write ─────────────────────────────────────────────────────────────\n progress(\"writing the favourite\");\n const db = this.#open();\n try {\n const favEnt = this.#entity(db, \"FavoriteItem\");\n const mixEnt = this.#entity(db, \"MixinMapItem\");\n const favPk = this.#nextPk(db, \"FavoriteItem\", \"ZFAVORITEITEM\");\n const mixPk = this.#nextPk(db, \"MixinMapItem\", \"ZMIXINMAPITEM\");\n const now = Date.now() / 1000 - CORE_DATA_EPOCH_OFFSET;\n const id = uuidBytes();\n const label = input.name ?? donor.name ?? input.query;\n const position = Number(\n (db.prepare(`SELECT COUNT(*) AS c FROM ZFAVORITEITEM`).get() as { c: number | bigint }).c,\n );\n\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n db.prepare(\n `INSERT INTO \"ZFAVORITEITEM\"\n (Z_PK, Z_ENT, Z_OPT, ZHIDDEN, ZPOSITIONINDEX, ZSOURCE, ZTYPE, ZVERSION,\n ZMAPITEM, ZMUID, ZCREATETIME, ZMODIFICATIONTIME, ZMAPITEMLASTREFRESHED,\n ZLATITUDE, ZLONGITUDE, ZMAPITEMNAME, ZMAPITEMADDRESS, ZMAPITEMCATEGORY,\n ZIDENTIFIER)\n VALUES (?, ?, 1, 0, ?, 0, 1, 2, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n favPk,\n favEnt,\n position,\n mixPk,\n donor.muid === null ? null : Number(donor.muid),\n now,\n now,\n now,\n donor.lat,\n donor.lon,\n label,\n donor.address,\n donor.category,\n id,\n );\n db.prepare(\n `INSERT INTO \"ZMIXINMAPITEM\"\n (Z_PK, Z_ENT, Z_OPT, ZFAVORITEITEM, ZCREATETIME, ZMODIFICATIONTIME,\n ZLATITUDE, ZLONGITUDE, ZMAPITEMSTORAGE)\n VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?)`,\n ).run(mixPk, mixEnt, favPk, now, now, donor.lat, donor.lon, donor.storage);\n // Not bumping these hands the next insert BY MAPS a duplicate primary\n // key, which is a corruption of the app's data by our omission.\n db.prepare(`UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_NAME = 'FavoriteItem'`).run(favPk);\n db.prepare(`UPDATE Z_PRIMARYKEY SET Z_MAX = ? WHERE Z_NAME = 'MixinMapItem'`).run(mixPk);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // Already resolved; the throw below is the report.\n }\n throw err;\n }\n\n return {\n rowId: favPk,\n uuid: uuidOf(id),\n name: label,\n latitude: Number(donor.lat),\n longitude: Number(donor.lon),\n created: true,\n seeded,\n };\n } finally {\n db.close();\n }\n }\n\n /**\n * Remove a favourite and the place record it owns.\n *\n * `Z_MAX` is deliberately NOT rolled back. Core Data never reuses a primary\n * key, and decrementing the counter would hand the next insert one that is\n * already spoken for by a row still referenced elsewhere.\n */\n removeFavorite(key: EntityKey): boolean {\n const db = this.#open();\n try {\n const where = \"uuid\" in key ? `HEX(ZIDENTIFIER) = ?` : `Z_PK = ?`;\n const param = \"uuid\" in key ? key.uuid.replaceAll(\"-\", \"\").toUpperCase() : key.rowId;\n const row = db\n .prepare(`SELECT Z_PK AS pk, ZMAPITEM AS mapPk FROM ZFAVORITEITEM WHERE ${where} LIMIT 1`)\n .get(param) as { pk: number; mapPk: number | null } | undefined;\n if (!row) return false;\n\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n if (row.mapPk !== null) {\n db.prepare(`DELETE FROM ZMIXINMAPITEM WHERE Z_PK = ?`).run(row.mapPk);\n }\n db.prepare(`DELETE FROM ZFAVORITEITEM WHERE Z_PK = ?`).run(row.pk);\n db.exec(\"COMMIT\");\n } catch (err) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n // Already resolved.\n }\n throw err;\n }\n return true;\n } finally {\n db.close();\n }\n }\n}\n","import type { Logger, ReadOnlyMode } from \"@mgcrea/mcp-apple-core\";\n\nimport type { Config } from \"../config.js\";\nimport { renderInstant, type Epoch } from \"./dates.js\";\nimport { MapsStoreUnavailableError } from \"./errors.js\";\nimport { locateStore, type LocateResult } from \"./locate.js\";\nimport { encodeCollectionRef, encodePlaceRef, type PlaceKind } from \"./ref.js\";\nimport type { EntityKey } from \"./store.js\";\nimport { openStore, type CollectionRow, type MapsStore, type PlaceRow } from \"./store.js\";\nimport { MapsWriter, type OpenUrl } from \"./write.js\";\n\n/**\n * The client.\n *\n * ## One lane, and the consequence of that\n *\n * Safari's orchestrator exists to keep two lanes from being mistaken for one\n * another. Maps has no second lane at all — the app ships no scripting\n * dictionary — so this class has the opposite job: making sure the ABSENCE of\n * the one lane is never mistaken for an absence of data.\n *\n * Without Full Disk Access every list would naturally come back `[]`, and an\n * empty `favorites` reads exactly like a person who has saved no places. So a\n * read with no store THROWS a named error; it never returns an empty array.\n * That is the same rule `packages/messages` follows, and for the same reason:\n * both are surfaces where the grant is not an optimisation.\n */\n\nexport type RenderedPlace = {\n ref: string;\n kind: PlaceKind;\n /** The user's own label when they set one, otherwise the place's name. */\n name: string | null;\n /** The place's own name, kept separate so a rename is visible as one. */\n placeName: string | null;\n address: string | null;\n latitude: number | null;\n longitude: number | null;\n /**\n * Apple's place identifier. Stable for the same place across devices, and\n * shared between a favourite and a collection entry for the same place — so\n * it is reported, and never used as the ref. See `ref.ts`.\n *\n * A STRING: it is a 64-bit integer that does not fit in a JS number. A real\n * store returned `-2679868148951248105`.\n */\n muid: string | null;\n created: string | null;\n modified: string | null;\n /**\n * False for a row with no linked place record. MEASURED: 3 of 23 favourites,\n * carrying no name and no coordinate — almost certainly the unconfigured\n * Home / Work / School slots Maps creates whether or not anyone fills them in.\n */\n linked: boolean;\n};\n\nexport type RenderedCollection = {\n ref: string;\n title: string | null;\n /** Maps' own count, which may include items this server cannot enumerate. */\n placesCount: number | null;\n created: string | null;\n modified: string | null;\n};\n\nexport type CreateClientOptions = {\n config: Config;\n logger?: Logger;\n /** Injected by tests so discovery never reaches the developer's real home. */\n home?: string;\n};\n\n/**\n * What diagnostics says about one entity: how many rows, which column won for\n * each field, and which fields resolved to nothing. `resolved` is the part that\n * matters when a result looks wrong — it names the column actually being read.\n */\nconst summariseEntity = (e: MapsStore[\"caps\"][\"favorites\"]) => ({\n rows: e.rows,\n resolved: Object.fromEntries(Object.entries(e.fields).filter(([, v]) => v !== null)),\n unresolved: Object.entries(e.fields)\n .filter(([, v]) => v === null)\n .map(([k]) => k),\n});\n\nexport class AppleMapsClient {\n readonly #config: Config;\n readonly #logger: Logger | undefined;\n readonly #home: string | undefined;\n\n #located: LocateResult | null = null;\n #store: MapsStore | null = null;\n #storeError: string | null = null;\n\n constructor(opts: CreateClientOptions) {\n this.#config = opts.config;\n this.#logger = opts.logger;\n this.#home = opts.home;\n }\n\n get config(): Config {\n return this.#config;\n }\n\n located(): LocateResult {\n this.#located ??= locateStore({\n storePath: this.#config.storePath,\n ...(this.#home ? { home: this.#home } : {}),\n });\n return this.#located;\n }\n\n /**\n * A writer bound to the located store, or an error explaining why not.\n *\n * Writes go through their own read-write connection rather than the shared\n * read-only one: `store()` opens with `query_only` and an `immutable` fallback\n * precisely so a read can never mutate by accident, and widening it would\n * throw that guarantee away for every read on the surface.\n */\n writer(openUrl?: OpenUrl): MapsWriter {\n const located = this.located();\n if (!located.readable || !located.storePath) {\n throw new MapsStoreUnavailableError(located.reason ?? \"Maps' store could not be opened.\");\n }\n return new MapsWriter({\n storePath: located.storePath,\n ...(openUrl ? { openUrl } : {}),\n });\n }\n\n /** Open the store, once, lazily. Every read goes through here. */\n store(): MapsStore {\n if (this.#store) return this.#store;\n if (this.#storeError !== null) throw new MapsStoreUnavailableError(this.#storeError);\n\n const located = this.located();\n if (!located.readable || !located.storePath) {\n this.#storeError = located.reason ?? \"Maps' store could not be opened.\";\n throw new MapsStoreUnavailableError(this.#storeError);\n }\n try {\n this.#store = openStore({\n path: located.storePath,\n mode: this.#config.indexMode as ReadOnlyMode,\n ...(this.#logger ? { logger: this.#logger } : {}),\n });\n return this.#store;\n } catch (err) {\n this.#storeError = err instanceof Error ? err.message : String(err);\n throw err;\n }\n }\n\n get epoch(): Epoch {\n return this.store().caps.epoch;\n }\n\n #render(kind: PlaceKind, row: PlaceRow, epoch: Epoch): RenderedPlace {\n return {\n ref: encodePlaceRef(kind, row.uuid ? { uuid: row.uuid } : { rowId: row.id }),\n kind,\n name: row.customName ?? row.name,\n placeName: row.name,\n address: row.address,\n latitude: row.latitude,\n longitude: row.longitude,\n muid: row.muid,\n created: renderInstant(row.createdRaw, epoch),\n modified: renderInstant(row.modifiedRaw, epoch),\n linked: row.linked,\n };\n }\n\n #renderCollection(row: CollectionRow, epoch: Epoch): RenderedCollection {\n return {\n ref: encodeCollectionRef(row.uuid ? { uuid: row.uuid } : { rowId: row.id }),\n title: row.title,\n placesCount: row.placesCount,\n created: renderInstant(row.createdRaw, epoch),\n modified: renderInstant(row.modifiedRaw, epoch),\n };\n }\n\n places(\n kind: PlaceKind,\n opts: {\n limit: number;\n collectionId?: number | undefined;\n query?: string | undefined;\n unfiled?: boolean | undefined;\n },\n ): { places: RenderedPlace[]; truncated: boolean; datesAvailable: boolean } {\n const store = this.store();\n const { rows, truncated } = store.places(kind, opts);\n return {\n places: rows.map((r) => this.#render(kind, r, store.caps.epoch)),\n truncated,\n datesAvailable: store.caps.epoch.confident,\n };\n }\n\n place(kind: PlaceKind, key: EntityKey): RenderedPlace | null {\n const store = this.store();\n const row = store.place(kind, key);\n return row ? this.#render(kind, row, store.caps.epoch) : null;\n }\n\n /**\n * A collection ref to the row id its items point at, or null when the ref\n * addresses nothing in this store.\n *\n * Collection membership is a Core Data foreign key and always holds `Z_PK`,\n * so a uuid ref has to be translated before it can filter items.\n */\n collectionRowId(key: EntityKey): number | null {\n return this.store().collectionRowId(key);\n }\n\n collections(opts: { limit: number }): {\n collections: RenderedCollection[];\n truncated: boolean;\n /** Null when the membership key was not found — see `store.ts`. */\n itemsEnumerable: boolean;\n /**\n * Saved places filed in no collection, or null when unanswerable.\n *\n * Reported beside the collections because the counts otherwise do not add\n * up and nothing says so: the guides account for 18 places while the store\n * holds 30, and a caller comparing the two has no way to learn where the\n * rest went.\n */\n unfiled: number | null;\n } {\n const store = this.store();\n const { rows, truncated } = store.collections(opts);\n return {\n collections: rows.map((r) => this.#renderCollection(r, store.caps.epoch)),\n truncated,\n itemsEnumerable: store.caps.membership !== null,\n unfiled: store.unfiledCount(),\n };\n }\n\n /**\n * Search every place-bearing entity at once.\n *\n * Three separate queries rather than a UNION, because the three tables have\n * different resolved columns and a UNION would have to flatten them to the\n * narrowest — which on this store means losing history's coordinates, since\n * they live in a differently named column from the other two.\n */\n search(opts: { query: string; limit: number }): {\n places: RenderedPlace[];\n truncated: boolean;\n datesAvailable: boolean;\n } {\n const store = this.store();\n const kinds: PlaceKind[] = [\"favorite\", \"collection-item\", \"history\"];\n const places: RenderedPlace[] = [];\n let truncated = false;\n for (const kind of kinds) {\n const { rows, truncated: t } = store.places(kind, {\n limit: opts.limit,\n query: opts.query,\n });\n truncated ||= t;\n for (const r of rows) places.push(this.#render(kind, r, store.caps.epoch));\n }\n return {\n places: places.slice(0, opts.limit),\n truncated: truncated || places.length > opts.limit,\n datesAvailable: store.caps.epoch.confident,\n };\n }\n\n /** Everything diagnostics needs, with no failure allowed to fail the call. */\n status(): {\n located: LocateResult;\n store: { opened: boolean; mode: string | null; reason: string | null };\n capabilities: Record<string, unknown> | null;\n } {\n const located = this.located();\n let store: MapsStore | null = null;\n let reason: string | null = located.reason;\n try {\n store = this.store();\n } catch (err) {\n reason = err instanceof Error ? err.message : String(err);\n }\n return {\n located,\n store: { opened: store !== null, mode: store?.mode ?? null, reason },\n capabilities: store\n ? {\n fingerprint: store.caps.fingerprint,\n tableCount: store.caps.tables.length,\n epoch: store.caps.epoch,\n collectionFk: store.caps.collectionFk,\n collectionMembership: store.caps.membership,\n entities: {\n favorites: summariseEntity(store.caps.favorites),\n collections: summariseEntity(store.caps.collections),\n collectionItems: summariseEntity(store.caps.collectionItems),\n history: summariseEntity(store.caps.history),\n mapItems: summariseEntity(store.caps.mapItems),\n },\n }\n : null,\n };\n }\n\n close(): void {\n this.#store?.close();\n this.#store = null;\n }\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, which is the whole point.\n *\n * `allowWrites` gates two mutating tools, and gates them harder than elsewhere.\n * Maps has no scripting dictionary and no registered App Intents, so a write is\n * SQL straight into a Core Data store that `NSPersistentCloudKitContainer`\n * mirrors — which means it reaches every device on the account, not just this\n * Mac. That was measured, along with the rule that keeps it safe: never\n * fabricate a place record, only ever copy one Maps wrote itself. See\n * `docs/maps.md` and `client/write.ts`.\n */\nconst ConfigSchema = BaseConfigSchema.extend({\n /** Explicit store path. Bypasses discovery — for tests and forensic copies. */\n storePath: z.string().optional(),\n indexMode: z.enum([\"auto\", \"ro\", \"immutable\", \"off\"]).default(\"auto\"),\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_MAPS_ALLOW_WRITES),\n exposePrompts: parseBool(env.APPLE_MAPS_EXPOSE_PROMPTS),\n debug: parseBool(env.APPLE_MAPS_DEBUG),\n storePath: trimmed(env.APPLE_MAPS_STORE),\n indexMode: trimmed(env.APPLE_MAPS_INDEX_MODE),\n osascriptPath: trimmed(env.APPLE_MAPS_OSASCRIPT_PATH),\n osascriptTimeoutMs: parseIntOpt(env.APPLE_MAPS_OSASCRIPT_TIMEOUT_MS),\n maxResults: parseIntOpt(env.APPLE_MAPS_MAX_RESULTS),\n });\n","/**\n * The Maps operating manual, served as `cupertino://maps/guide` and embedded\n * ahead of every Maps prompt. Static by design — see the note in the Mail guide.\n */\nexport const MAPS_GUIDE = `# Maps — how to drive this server\n\n## What this surface is, and is not\n\nIt reads the places **saved on this Mac**: favourites, collections (Guides) and\nrecents. It does **not** search Apple's map of the world, geocode an address,\ngive directions, or compute a travel time. If asked for any of those, say so\nplainly rather than searching the saved places and presenting a near-miss —\n\"the nearest coffee shop\" is not a question this server can answer, and a\nfavourite called \"Coffee\" is not the answer to it.\n\n## One lane, and no fallback\n\nMaps ships **no scripting dictionary** — there is no \\`.sdef\\` in the app bundle\n— so unlike every other surface here there is no Apple Events lane to fall back\nto. Everything comes from a Core Data store under **Full Disk Access**.\n\nThe consequence matters: without the grant this server returns an **error**, not\nan empty list. If a tool ever does return an empty list, that means the user has\ngenuinely saved nothing of that kind. Check\n\\`cupertino://maps/diagnostics\\` before concluding either way.\n\n## Which tool answers which question\n\n| Question | Tool |\n| --- | --- |\n| What places has the user saved? | \\`apple_maps_list_favorites\\` |\n| What Guides do they keep? | \\`apple_maps_list_collections\\` |\n| What is filed in one Guide? | \\`apple_maps_list_collection_places\\` |\n| Where have they looked recently? | \\`apple_maps_list_recents\\` |\n| Find a saved place by name or address | \\`apple_maps_search_places\\` |\n| One place, in full | \\`apple_maps_get_place\\` |\n\n## Four things not to say\n\n**\\`linked: false\\` is not a broken row.** Some favourites have no place\nattached, no name and no coordinate. They are Maps' unconfigured Home / Work /\nSchool slots. Report them as unset, not as places.\n\n**A null date is unknown, not old.** Timestamps are placed on an epoch detected\nfrom the store. When detection fails every date reads \\`null\\` rather than being\nguessed, because a date wrong by 31 years reads exactly like a correct one.\n\n**A collection with no listable places may still have places.** How an item\nbelongs to a collection is not exposed by every version of this store. When the\nkey is missing, \\`placesCount\\` is still Maps' own accurate number and the\nplaces simply cannot be enumerated. Say that, rather than reporting an empty\nGuide.\n\n**Refs expire.** They address a row in a store that iCloud re-syncs, and Core\nData reuses row ids after a delete. A ref is good for this conversation. Do not\nstore one or hand it back later.\n\n## Privacy, worth holding in mind\n\nSaved places are a home address, a doctor, a school, a partner's flat. This is\namong the most sensitive data any surface here reads. Answer what was asked;\ndo not enumerate everything because a listing tool is available.\n`;\n","import { promptArg, registerWorkflowPrompt, type PromptContext } from \"@mgcrea/mcp-apple-core\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { MAPS_GUIDE } from \"./guide.js\";\n\nconst CTX: PromptContext = { surface: \"maps\", guide: MAPS_GUIDE };\n\n/**\n * Maps' one workflow prompt.\n *\n * It exists because \"where was that place\" spans three lists that hold the same\n * kind of thing for different reasons — a favourite is deliberate, a collection\n * entry is filed, a recent is incidental — and searching only one produces a\n * confident answer to a question nobody asked. The prompt also carries the\n * refusal: this server does not know about places the user has never saved, and\n * the most likely way to be wrong here is to answer anyway.\n */\nexport const registerPrompts = (server: McpServer): void => {\n registerWorkflowPrompt(server, CTX, {\n name: \"apple_maps_where_was_that_place\",\n title: \"Find a place I saved\",\n description:\n \"Track down a saved place across favourites, Guides and recents — three lists that hold \" +\n \"places for different reasons. Read-only.\",\n argsSchema: {\n about: promptArg('What the place was, e.g. \"the ramen place near the office\".'),\n },\n build: ({ about }) => `Find the saved place ${about ? `matching: ${about}` : \"I am after\"}.\n\nStart with \\`apple_maps_search_places\\` — it covers favourites, Guide entries and\nrecents in one call, and tells you which kind each match came from. That\ndistinction is the answer, not a detail: a favourite is somewhere I chose to\nkeep, a recent is somewhere I merely looked at once.\n\nIf that finds nothing, list the three directly before concluding it is not\nthere — a place I saved under my own label (\"Mum's\") will not match a search for\nwhat it actually is.\n\nThen report what you found, with the address and how it was saved.\n\nThree things to get right:\n- If nothing matches, say I have not saved it. Do **not** fall back to guessing\n a place from general knowledge — this server only knows what is on this Mac,\n and inventing an address is the worst possible failure here.\n- An entry with \\`linked: false\\` is an unconfigured Home/Work/School slot, not\n a place. Do not report it as one.\n- If a date reads null, the date is unknown. Do not describe it as old.\n\nIf every list comes back empty, check \\`cupertino://maps/diagnostics\\` before\nconcluding I have saved nothing — this surface has no second lane, so a missing\nFull Disk Access grant takes all of it at once.`,\n });\n};\n","/**\n * Maps' tool helpers. The generic half lives in `@mgcrea/mcp-apple-core`;\n * re-exported here so tools import from one place.\n */\nexport {\n compact,\n fail,\n limitArg,\n ok,\n okText,\n toFailure,\n wrap,\n wrapResult,\n type ToolResult,\n} from \"@mgcrea/mcp-apple-core\";\n\nimport { z } from \"zod\";\n\nexport const placeRefArg = z\n .string()\n .min(1)\n .describe(\n 'An opaque place ref from a listing (looks like \"p1:f:12\"). Do not construct one by hand. ' +\n \"Refs address a row in the local store, so they are only valid for this session — an \" +\n \"iCloud re-sync can renumber them.\",\n );\n\nexport const collectionRefArg = z\n .string()\n .min(1)\n .describe('An opaque collection ref from apple_maps_list_collections (looks like \"pc1:3\").');\n\nexport const queryArg = z\n .string()\n .min(1)\n .describe(\n \"Text to look for in a place's name, the label you gave it, or its address. Wildcards are \" +\n 'escaped, so searching for \"100%\" finds that literal string.',\n );\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"../build-info.js\";\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { wrap } from \"./util.js\";\n\n/**\n * Build the report.\n *\n * Split out of the tool registration so the `cupertino://maps/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: AppleMapsClient,\n): Promise<Record<string, unknown>> => {\n const status = client.status();\n const located = status.located;\n return {\n server: { name: BUILD_INFO.name, version: BUILD_INFO.version },\n settings: { exposePrompts: client.config.exposePrompts },\n lanes: {\n summary:\n \"Maps has ONE lane. Maps.app ships no scripting dictionary — there is no .sdef in the \" +\n \"bundle, checked directly — so there is no Apple Events fallback. Without Full Disk \" +\n \"Access this server cannot read anything at all, and it says so rather than returning \" +\n \"empty lists.\",\n fileLane: {\n needs: \"Full Disk Access\",\n answers: \"favourites, collections (guides), recents\",\n working: status.store.opened,\n },\n appleEvents: \"none — Maps is not scriptable\",\n writes:\n \"none — this server registers no mutating tool. The store is mirrored to iCloud by \" +\n \"NSPersistentCloudKitContainer, so a write is an edit to one replica of a \" +\n \"synchronising graph underneath a running app. That was never probed.\",\n },\n store: {\n path: located.storePath,\n exists: located.exists,\n readable: located.readable,\n opened: status.store.opened,\n mode: status.store.mode,\n reason: status.store.reason,\n resolvedByScan: located.resolvedByScan,\n ...status.capabilities,\n },\n files: {\n directory: located.directory,\n localCache: {\n path: located.localCache.path,\n exists: located.localCache.exists,\n note:\n \"The device-local cache. Same entities as the sync store and, on the probed \" +\n \"machine, zero rows in every one of them. Located so it is visibly considered, \" +\n \"never opened.\",\n },\n },\n caveats: [\n \"This store was missed three times before it was found, and the reasons are worth \" +\n \"knowing if it ever looks absent: the file has NO EXTENSION (MapsSync_0.0.1), it \" +\n \"lives in the one directory of Maps' container that Full Disk Access gates, and \" +\n \"`group.com.apple.Maps` is a decoy that is EPERM rather than empty.\",\n \"Columns are resolved BY COVERAGE, not by name. ZHISTORYITEM carries both ZLATITUDE \" +\n \"(1 row of 33) and ZLATITUDE1 (19 of 33); picking the first recognised name would \" +\n \"report that Maps holds almost no coordinates. `entities.*.resolved` above shows \" +\n \"which column actually won for each field.\",\n \"Some favourites have no linked place: 3 of 23 on the probed machine, with no name \" +\n \"and no coordinate. Almost certainly the unconfigured Home / Work / School slots. \" +\n \"They are returned with `linked: false` rather than dropped, because silently \" +\n \"omitting rows reads as a deletion.\",\n \"Collection membership is a many-to-many join table, Z_6PLACES(Z_6COLLECTIONS, \" +\n \"Z_7PLACES) on the probed machine, NOT a column on ZCOLLECTIONITEM — which is why \" +\n \"four guessed column names all missed it. It is re-proved at open time by \" +\n \"reproducing ZPLACESCOUNT exactly, never by matching a name, because \" +\n \"ZCOLLECTIONITEM.ZMAPITEM joins 3 of 10 collections by coincidence. When nothing \" +\n \"reproduces those counts, `collectionMembership` is null, collections list \" +\n \"without their places and every result says so.\",\n \"Refs address a local row id. Core Data reuses those after a delete and this store is \" +\n \"mirrored from CloudKit, so a re-sync can renumber rows. A ref is good for the \" +\n \"current session and should not be stored.\",\n \"Timestamps are placed on an epoch DETECTED from the store, never assumed. The same \" +\n \"value read as unix seconds instead of Core Data seconds lands in 1995 and looks \" +\n \"entirely plausible. When detection fails every date reads null rather than guessed.\",\n ],\n };\n};\n\nexport const registerDiagnosticsTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_diagnostics\",\n {\n description:\n \"Report whether Maps' store can be read, which columns were resolved for each entity, \" +\n \"and what this server deliberately cannot do. Start here when a read returns nothing — \" +\n \"this surface has no second lane, so a missing grant means no data at all rather than \" +\n \"slower data.\",\n inputSchema: {},\n annotations: { readOnlyHint: true },\n },\n async () => wrap(() => buildDiagnostics(client)),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { decodeCollectionRef, decodePlaceRef } from \"../client/ref.js\";\nimport {\n collectionRefArg,\n compact,\n fail,\n limitArg,\n ok,\n placeRefArg,\n queryArg,\n wrapResult,\n} from \"./util.js\";\n\n/**\n * The place tools.\n *\n * Every description states the permission fact plainly. On this surface it is\n * not the usual \"slower without the grant\" — there is no second lane, so\n * without Full Disk Access these tools have nothing to read. Saying so in the\n * description is what stops a model concluding, from an error it half-read,\n * that the person simply has not saved any places.\n */\nexport const registerPlaceTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_list_favorites\",\n {\n description:\n \"List the places saved as favourites in Maps, with coordinates, address and the label \" +\n \"the user gave them. Needs Full Disk Access — Maps is not scriptable, so without the \" +\n \"grant this returns an error rather than an empty list. Some entries have no linked \" +\n \"place (`linked: false`); those are the unconfigured Home/Work/School slots, not \" +\n \"broken rows. This is everything SAVED: Maps' own Pinned panel applies display rules \" +\n \"that are not in the store and can show fewer.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.places(\"favorite\", { limit: limit ?? client.config.maxResults });\n const unlinked = result.places.filter((p) => !p.linked).length;\n return ok(\n compact({\n favorites: result.places,\n count: result.places.length,\n unlinked: unlinked\n ? `${unlinked} entr${unlinked === 1 ? \"y has\" : \"ies have\"} no linked place — ` +\n `these are Maps' unconfigured Home/Work/School slots. They are returned rather ` +\n `than dropped, because silently omitting rows reads as a deletion.`\n : undefined,\n /**\n * MEASURED, and the reason the note above no longer claims this count\n * matches the app: on a real store Maps' Pinned panel showed 17 while\n * this tool returned 24. Three extras were the unlinked slots; four\n * were ordinary favourites with names, addresses and coordinates, one\n * an exact duplicate of a shown entry.\n *\n * No column in ZFAVORITEITEM separates them — ZHIDDEN, ZSOURCE, ZTYPE\n * and ZVERSION were all cross-tabulated and none yields a group of\n * four. `ZVERSION = 2` has exactly 17 rows and is a COINCIDENCE OF\n * TOTALS: 16 of them are linked while all 17 shown entries are. So\n * Maps applies display rules this server cannot see — de-duplication\n * is the visible one — and no filter written here could reproduce\n * them. Saying so beats guessing, and beats the old note, which\n * asserted the opposite of what was measured. See docs/maps.md.\n */\n mayExceedApp:\n \"Maps' own Pinned panel can show FEWER places than this. It applies display \" +\n \"rules — de-duplication at least — that are not recorded in the store, so this \" +\n \"is everything saved, not everything shown.\",\n truncated: result.truncated\n ? `More favourites exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n datesUnavailable: result.datesAvailable\n ? undefined\n : \"Timestamps could not be placed on a known epoch, so every date reads null. \" +\n \"They are withheld rather than guessed — see apple_maps_diagnostics.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_collections\",\n {\n description:\n \"List the collections (Guides) in Maps. Each carries Maps' own count of the places in \" +\n \"it. Use apple_maps_list_collection_places to enumerate one. Needs Full Disk Access.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.collections({ limit: limit ?? client.config.maxResults });\n return ok(\n compact({\n collections: result.collections,\n count: result.collections.length,\n // The guides do not account for every saved place, and silence\n // about that reads as \"there are no others\".\n unfiled:\n result.unfiled && result.unfiled > 0\n ? `${result.unfiled} saved place(s) are in no collection. ` +\n `List them with apple_maps_list_unfiled_places.`\n : undefined,\n // The honest version of a capability this server may not have.\n itemsUnavailable: result.itemsEnumerable\n ? undefined\n : \"This store does not expose how an item belongs to a collection, so the places \" +\n \"inside a collection cannot be listed. `placesCount` is Maps' own number and is \" +\n \"still accurate; apple_maps_list_collection_places will return nothing.\",\n truncated: result.truncated\n ? `More collections exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_collection_places\",\n {\n description:\n \"List the places filed in one collection, by its ref. Needs Full Disk Access. Returns \" +\n \"nothing when this store does not expose collection membership — check \" +\n \"apple_maps_diagnostics, which reports whether the key was resolved.\",\n inputSchema: { ref: collectionRefArg, limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ ref, limit }) =>\n wrapResult(async () => {\n const collectionId = client.collectionRowId(decodeCollectionRef(ref));\n const result = client.places(\"collection-item\", {\n limit: limit ?? client.config.maxResults,\n collectionId: collectionId ?? undefined,\n });\n const collections = client.collections({ limit: 1_000 });\n if (!collections.itemsEnumerable) {\n return fail(\n \"This store does not expose which collection an item belongs to, so its places \" +\n \"cannot be listed. The collection itself and its place count are still readable \" +\n \"through apple_maps_list_collections.\",\n );\n }\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More places exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_unfiled_places\",\n {\n description:\n \"List saved places that are in no collection. Maps files a saved place into a Guide \" +\n \"through a join table, and a place can exist with no row there — 12 of 30 on the probed \" +\n \"machine, 7 of which appear nowhere else in the store: not as a favourite, not in \" +\n \"another Guide, not in recents. Those are reachable through no other tool. Needs Full \" +\n \"Disk Access. Returns nothing when collection membership could not be resolved, which \" +\n \"apple_maps_diagnostics reports.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const collections = client.collections({ limit: 1 });\n if (!collections.itemsEnumerable) {\n return fail(\n \"This store does not expose which collection an item belongs to, so places in no \" +\n \"collection cannot be told apart from places in one. \" +\n \"apple_maps_list_collections still works.\",\n );\n }\n const result = client.places(\"collection-item\", {\n limit: limit ?? client.config.maxResults,\n unfiled: true,\n });\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More places exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_list_recents\",\n {\n description:\n \"List the places recently looked at in Maps, newest first. This is Maps' Recents list, \" +\n \"not a search history: it holds places, directions and searches the user actually \" +\n \"opened. Needs Full Disk Access.\",\n inputSchema: { limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ limit }) =>\n wrapResult(async () => {\n const result = client.places(\"history\", { limit: limit ?? client.config.maxResults });\n const named = result.places.filter((p) => p.name).length;\n return ok(\n compact({\n recents: result.places,\n count: result.places.length,\n // MEASURED: 1 of 33 rows carried a name. A recent's place name\n // lives in the linked map item's undecoded blob, not in a column,\n // so most rows have coordinates and no label. Saying so stops a\n // caller reporting \"33 unnamed places\" as if the data were damaged.\n namesUnavailable:\n result.places.length > 0 && named * 4 < result.places.length\n ? `Only ${named} of ${result.places.length} entries carry a name. Maps keeps a ` +\n `recent's place name in an encoded record this server does not decode, so most ` +\n `rows have coordinates and dates but no label. This is the store's shape, not ` +\n `missing data.`\n : undefined,\n truncated: result.truncated\n ? `More entries exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n datesUnavailable: result.datesAvailable\n ? undefined\n : \"Timestamps could not be placed on a known epoch, so every date reads null.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_search_places\",\n {\n description:\n \"Search every saved place — favourites, collection entries and recents — by name, by \" +\n \"the label the user gave it, or by address. Returns each match with the kind of entry \" +\n \"it came from. Needs Full Disk Access. This searches what is SAVED on this Mac; it \" +\n \"does not search Apple's map of the world.\",\n inputSchema: { query: queryArg, limit: limitArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ query, limit }) =>\n wrapResult(async () => {\n const result = client.search({ query, limit: limit ?? client.config.maxResults });\n return ok(\n compact({\n places: result.places,\n count: result.places.length,\n truncated: result.truncated\n ? `More matches exist beyond limit=${limit ?? client.config.maxResults}.`\n : undefined,\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_get_place\",\n {\n description:\n \"Get one saved place by its ref, with coordinates, address and dates. Needs Full Disk \" +\n \"Access.\",\n inputSchema: { ref: placeRefArg },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ ref }) =>\n wrapResult(async () => {\n const { kind, key } = decodePlaceRef(ref);\n const place = client.place(kind, key);\n if (!place) {\n return fail(\n \"uuid\" in key\n ? `No place for that ref. It was removed in Maps, or it belongs to a different ` +\n `store than the one being read.`\n : `No place for that ref. It may have been removed in Maps, or iCloud may have ` +\n `re-synced the store and renumbered the rows since the listing ran — this ref ` +\n `carries a row id, which only this store's current numbering can resolve. ` +\n `Re-run the listing for a current ref.`,\n );\n }\n return ok({ place });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { decodePlaceRef } from \"../client/ref.js\";\nimport { compact, fail, ok, placeRefArg, wrapResult } from \"./util.js\";\n\n/**\n * The mutating tools.\n *\n * Registered only when `APPLE_MAPS_ALLOW_WRITES` is true, so a host that has not\n * opted in is never told they exist — the same gate every other surface uses.\n *\n * ## Two things these descriptions must say, because they are surprising\n *\n * **Adding a favourite leaves an entry in Recents.** A place is only real to\n * Maps once it has a GEO record, and the only thing that can produce one is Maps\n * itself: the place is opened through the `maps://` URL scheme, Maps resolves it\n * and files it in Recents, and that record is copied into the favourite. There\n * is no way to have the first without the second, so the caller is told rather\n * than surprised. When the place is already known to the store, no seeding\n * happens and no Recents entry appears.\n *\n * **It reaches the user's other devices.** The store is mirrored by\n * `NSPersistentCloudKitContainer`, which reconciles on the app's next save\n * whether or not it was told anything. A favourite added here arrived on an\n * iPhone. That makes this the one write in the bundle whose blast radius is\n * larger than the machine it runs on, and the descriptions say so.\n */\nexport const registerWriteTools = (server: McpServer, client: AppleMapsClient): void => {\n server.registerTool(\n \"apple_maps_add_favorite\",\n {\n description:\n \"Save a place to Maps' favourites (the Pinned list). Give the place NAME as `query` — a \" +\n \"bare coordinate does not identify a place to Maps — and latitude/longitude when known, \" +\n \"which makes the match exact and skips the lookup. SIDE EFFECT: unless the place is \" +\n \"already in the store, Maps is asked to resolve it and the place also appears in the \" +\n \"user's Recents; there is no way to add a favourite without that. The favourite syncs \" +\n \"to the user's other Apple devices through iCloud. Idempotent: asking twice for the same \" +\n \"place returns the existing favourite rather than creating a second. Needs Full Disk \" +\n \"Access.\",\n inputSchema: {\n query: z\n .string()\n .min(1)\n .describe(\"The place's name, as you would type it into Maps' search field.\"),\n latitude: z.number().min(-90).max(90).optional().describe(\"Latitude, when known.\"),\n longitude: z.number().min(-180).max(180).optional().describe(\"Longitude, when known.\"),\n name: z.string().min(1).optional().describe(\"Label to save it under. Defaults to `query`.\"),\n },\n annotations: { readOnlyHint: false, idempotentHint: true },\n },\n async ({ query, latitude, longitude, name }) =>\n wrapResult(async () => {\n const result = client.writer().addFavorite({ query, latitude, longitude, name });\n return ok(\n compact({\n favorite: {\n ref: `p1:f:${result.uuid.replaceAll(\"-\", \"\")}`,\n name: result.name,\n latitude: result.latitude,\n longitude: result.longitude,\n },\n created: result.created,\n alreadyExisted: result.created\n ? undefined\n : \"A favourite for this place already existed and was returned unchanged.\",\n recentsNote: result.seeded\n ? \"Maps was asked to resolve this place, so it now also appears in the user's \" +\n \"Recents. That is how the place record is obtained and cannot be avoided.\"\n : undefined,\n syncNote:\n \"This favourite will reach the user's other Apple devices through iCloud once \" +\n \"Maps next runs.\",\n }),\n );\n }),\n );\n\n server.registerTool(\n \"apple_maps_remove_favorite\",\n {\n description:\n \"Remove a place from Maps' favourites, by a ref from apple_maps_list_favorites. This \" +\n \"deletes the favourite on the user's other Apple devices too, through iCloud. It does \" +\n \"not affect Guides, Recents, or anything else that references the same place. Needs \" +\n \"Full Disk Access.\",\n inputSchema: { ref: placeRefArg },\n annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true },\n },\n async ({ ref }) =>\n wrapResult(async () => {\n const { kind, key } = decodePlaceRef(ref);\n if (kind !== \"favorite\") {\n return fail(\n `That ref points at a ${kind === \"history\" ? \"recent\" : \"collection entry\"}, not a ` +\n `favourite. Only favourites can be removed; pass a ref from ` +\n `apple_maps_list_favorites.`,\n );\n }\n const removed = client.writer().removeFavorite(key);\n if (!removed) {\n return fail(\n \"No favourite matched that ref. It may already have been removed, or the listing \" +\n \"it came from may predate a change. Re-run apple_maps_list_favorites.\",\n );\n }\n return ok({\n removed: true,\n syncNote:\n \"The removal will reach the user's other Apple devices through iCloud once Maps \" +\n \"next runs.\",\n });\n }),\n );\n};\n","import type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport type { AppleMapsClient } from \"../client/maps.js\";\nimport { registerDiagnosticsTools } from \"./diagnostics.js\";\nimport { registerPlaceTools } from \"./places.js\";\nimport { registerWriteTools } from \"./writes.js\";\n\nexport type ToolContext = {\n /**\n * Gates the two mutating tools, which are NOT registered when it is false —\n * so a host that has not opted in is never told they exist.\n *\n * This surface writes SQL directly into Maps' Core Data store, because Maps\n * has no scripting dictionary and no App Intents to write through. The store\n * is mirrored to iCloud, so a write here reaches every device on the account.\n * `docs/maps.md` carries the measurements; `client/write.ts` carries the rule\n * that makes it safe — never fabricate a place record, only copy one Maps\n * wrote itself.\n */\n allowWrites: boolean;\n};\n\n/**\n * Register the Apple Maps tools.\n *\n * The registered set does NOT vary with whether the store opened. That is a\n * runtime condition and MCP clients cache the tool list, so a list that shrank\n * without Full Disk Access would stay shrunk after the grant was given. Every\n * tool is registered on a machine with no grant at all; each fails with an\n * error that says which permission is missing.\n */\nexport const registerTools = (\n server: McpServer,\n client: AppleMapsClient,\n ctx: ToolContext,\n): void => {\n registerDiagnosticsTools(server, client);\n registerPlaceTools(server, client);\n if (!ctx.allowWrites) return;\n registerWriteTools(server, client);\n};\n","import { registerSurfaceResources, type Logger } from \"@mgcrea/mcp-apple-core\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n\nimport { BUILD_INFO } from \"./build-info.js\";\nimport { AppleMapsClient } from \"./client/maps.js\";\nimport type { Config } from \"./config.js\";\nimport { MAPS_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 discovery never reaches the developer's real home. */\n home?: string;\n};\n\nexport type CreatedServer = {\n server: McpServer;\n client: AppleMapsClient;\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 *\n * There is no `osascript` seam here, unlike every other surface. This server\n * never spawns one — Maps is not scriptable, so there is no Apple Events lane\n * to inject a fake for. Its absence is the point.\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 AppleMapsClient({\n config,\n ...(opts.logger ? { logger: opts.logger } : {}),\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://maps/guide` that this\n * server does not serve.\n */\n if (config.exposePrompts) {\n registerPrompts(server);\n registerSurfaceResources(server, {\n surface: \"maps\",\n displayName: \"Maps\",\n guide: MAPS_GUIDE,\n diagnostics: () => buildDiagnostics(client),\n // No inventory. Favourites, collections and recents are three queries\n // rather than three named containers, and a caller does not address them\n // by name the way they address a mailbox or a folder.\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-maps",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Model Context Protocol server for the macOS Apple Maps app",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apple",
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"places"
|
|
16
16
|
],
|
|
17
17
|
"homepage": "https://cupertino.mgcrea.io",
|
|
18
|
-
"bugs": "https://github.com/mgcrea/
|
|
18
|
+
"bugs": "https://github.com/mgcrea/cupertino/issues",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "Olivier Louvignes <olivier@mgcrea.io>",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/mgcrea/
|
|
23
|
+
"url": "git+https://github.com/mgcrea/cupertino.git",
|
|
24
24
|
"directory": "packages/maps"
|
|
25
25
|
},
|
|
26
26
|
"bin": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
51
51
|
"zod": "^4.4.3",
|
|
52
|
-
"@mgcrea/mcp-apple-core": "^1.
|
|
52
|
+
"@mgcrea/mcp-apple-core": "^1.6.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^26.2.0",
|