@mgcrea/mcp-apple-maps 1.15.0 → 1.17.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/README.md +18 -3
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +125 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-CpYiHM_a.js → server-C_leu2E0.js} +473 -15
- package/dist/server-C_leu2E0.js.map +1 -0
- package/package.json +2 -2
- package/dist/server-CpYiHM_a.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppleAutomationError, AppleAutomationError as AppleMapsError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, SchemaDriftError, SchemaDriftError as SchemaDriftError$1, columnsOf, compact, describeStore, detectEpoch, escapeLike, fail, fingerprintSchema, inspectFile, limitArg, ok, openReadOnly, parseBool, parseConfig, parseIntOpt, promptArg, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, resolveLimit, trimmed, withLazyTools, wrap, wrapResult } from "@mgcrea/mcp-apple-core";
|
|
1
|
+
import { AppleAutomationError, AppleAutomationError as AppleMapsError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, SchemaDriftError, SchemaDriftError as SchemaDriftError$1, columnsOf, compact, describeStore, detectEpoch, escapeLike, fail, fingerprintSchema, inspectFile, interferenceNote, limitArg, ok, openAxChannel, openReadOnly, parseBool, parseConfig, parseIntOpt, promptArg, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, resolveLimit, trimmed, watchInterference, withLazyTools, wrap, wrapResult } from "@mgcrea/mcp-apple-core";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -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-09-
|
|
18
|
+
gitCommit: "d05a45c",
|
|
19
|
+
gitCommitDate: "2026-09-07T16:21:19+02:00"
|
|
20
20
|
};
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/client/dates.ts
|
|
@@ -238,6 +238,296 @@ const locateStore = (opts = {}) => {
|
|
|
238
238
|
};
|
|
239
239
|
};
|
|
240
240
|
//#endregion
|
|
241
|
+
//#region src/client/ax.ts
|
|
242
|
+
/**
|
|
243
|
+
* Maps' place card, driven through Cupertino's native Accessibility driver.
|
|
244
|
+
*
|
|
245
|
+
* ## Why this exists beside the SQL lane rather than replacing it
|
|
246
|
+
*
|
|
247
|
+
* `client/write.ts` writes favourites by copying a record Maps minted and
|
|
248
|
+
* INSERTing it. That lane is correct and stays: pressing the card's `AddButton`
|
|
249
|
+
* does not create a favourite. `docs/desktop.md` measured it — the press lands
|
|
250
|
+
* an **unfiled saved place** and `ZFAVORITEITEM` was 26 rows before and 26
|
|
251
|
+
* after. The two lanes address different objects.
|
|
252
|
+
*
|
|
253
|
+
* What this reaches is the set the SQL lane does not offer at all:
|
|
254
|
+
*
|
|
255
|
+
* * **The Places library** — `add_to_places` / `delete_from_places`.
|
|
256
|
+
* * **Guide membership** — `docs/maps.md` lists it as unbuilt: "Adding a place
|
|
257
|
+
* to a collection needs one `Z_6PLACES` join row".
|
|
258
|
+
*
|
|
259
|
+
* ## The safety argument, which is the real reason to prefer it here
|
|
260
|
+
*
|
|
261
|
+
* The store is mirrored by `NSPersistentCloudKitContainer` and mirroring does
|
|
262
|
+
* not wait to be told, so a malformed row is not a local mistake — it reaches
|
|
263
|
+
* every device on the account. `write.ts` carries the rule that keeps SQL safe:
|
|
264
|
+
* never fabricate a place record, only copy one Maps wrote. **A write made
|
|
265
|
+
* through the interface cannot be malformed at all**, because Maps performs it.
|
|
266
|
+
* Where both lanes could do a job, this one has the smaller blast radius.
|
|
267
|
+
*
|
|
268
|
+
* ## The cost, which is the same one the SQL lane pays
|
|
269
|
+
*
|
|
270
|
+
* Reaching the card means opening `maps://?q=<name>&ll=<lat>,<lon>`, so the
|
|
271
|
+
* place is left in **Recents** whether or not anything is kept. That is the
|
|
272
|
+
* mechanism rather than an oversight, and every tool built on this says so.
|
|
273
|
+
*/
|
|
274
|
+
const MAPS_BUNDLE = "com.apple.Maps";
|
|
275
|
+
/** Identifiers Maps sets on the card. Unlocalised, unlike every name here. */
|
|
276
|
+
const CARD = {
|
|
277
|
+
add: "AddButton",
|
|
278
|
+
more: "MoreButton",
|
|
279
|
+
card: "PlaceCardViewController",
|
|
280
|
+
addToGuides: "add_to_guides",
|
|
281
|
+
deleteFromPlaces: "delete_from_places",
|
|
282
|
+
addToPlaces: "add_to_places",
|
|
283
|
+
picker: "GuidesPickerView",
|
|
284
|
+
guideRow: "UserGuidesPickerRowCell",
|
|
285
|
+
done: "CardButtonTypeDone",
|
|
286
|
+
close: "CardButtonTypeClose"
|
|
287
|
+
};
|
|
288
|
+
const defaultOpenUrl$1 = (url) => {
|
|
289
|
+
execFileSync("/usr/bin/open", ["-g", url], {
|
|
290
|
+
timeout: 1e4,
|
|
291
|
+
stdio: "ignore"
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* `"Favorites, 3 places"` -> `{ name: "Favorites", places: 3 }`.
|
|
296
|
+
*
|
|
297
|
+
* The count is a bonus and the name is the point, so a row whose shape is not
|
|
298
|
+
* recognised keeps its whole label as the name rather than being dropped. A
|
|
299
|
+
* guide that cannot be matched is worse than one with an odd name.
|
|
300
|
+
*/
|
|
301
|
+
const parseGuideRow = (label) => {
|
|
302
|
+
const match = /^(.*),\s*(\d+)\s+places?$/.exec(label);
|
|
303
|
+
if (!match?.[1]) return {
|
|
304
|
+
name: label,
|
|
305
|
+
places: null
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
name: match[1],
|
|
309
|
+
places: Number(match[2])
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
const DEFAULT_WAITS = {
|
|
313
|
+
card: 1e4,
|
|
314
|
+
sheet: 4e3
|
|
315
|
+
};
|
|
316
|
+
var MapsAxLane = class MapsAxLane {
|
|
317
|
+
#channel;
|
|
318
|
+
#openUrl;
|
|
319
|
+
#waits;
|
|
320
|
+
constructor(channel, openUrl, waits) {
|
|
321
|
+
this.#channel = channel;
|
|
322
|
+
this.#openUrl = openUrl;
|
|
323
|
+
this.#waits = waits;
|
|
324
|
+
}
|
|
325
|
+
/** Null when this server is not hosted by Cupertino — see `core/src/ax.ts`. */
|
|
326
|
+
static open(env = process.env, openUrl = defaultOpenUrl$1, waits = {}) {
|
|
327
|
+
const channel = openAxChannel(MAPS_SURFACE, env);
|
|
328
|
+
return channel ? new MapsAxLane(channel, openUrl, {
|
|
329
|
+
...DEFAULT_WAITS,
|
|
330
|
+
...waits
|
|
331
|
+
}) : null;
|
|
332
|
+
}
|
|
333
|
+
close() {
|
|
334
|
+
this.#channel.close();
|
|
335
|
+
}
|
|
336
|
+
/** Start watching for someone using the Mac during a sequence. */
|
|
337
|
+
watch() {
|
|
338
|
+
return watchInterference(this.#channel);
|
|
339
|
+
}
|
|
340
|
+
#call(tool, args = {}) {
|
|
341
|
+
return this.#channel.call({
|
|
342
|
+
tool: `apple_desktop_${tool}`,
|
|
343
|
+
args
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async #tree() {
|
|
347
|
+
return (await this.#call("ui_tree", {
|
|
348
|
+
bundleId: MAPS_BUNDLE,
|
|
349
|
+
detail: "all",
|
|
350
|
+
maxDepth: 20,
|
|
351
|
+
maxNodes: 4e3,
|
|
352
|
+
budgetSeconds: 20
|
|
353
|
+
})).elements ?? [];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Wait for an element rather than for a duration.
|
|
357
|
+
*
|
|
358
|
+
* `docs/desktop.md` calls this the trap that cost it the most: a card's chrome
|
|
359
|
+
* appears before its content, so no fixed settle time is correct. Cheap here —
|
|
360
|
+
* a whole Maps walk is ~0.17 s.
|
|
361
|
+
*/
|
|
362
|
+
async #poll(match, timeoutMs = this.#waits.card) {
|
|
363
|
+
const deadline = Date.now() + timeoutMs;
|
|
364
|
+
let lastError = null;
|
|
365
|
+
for (;;) {
|
|
366
|
+
try {
|
|
367
|
+
const found = (await this.#tree()).find(match);
|
|
368
|
+
if (found) return found;
|
|
369
|
+
lastError = null;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
lastError = error;
|
|
372
|
+
}
|
|
373
|
+
if (Date.now() >= deadline) {
|
|
374
|
+
if (lastError) throw lastError;
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Open a place and wait for its card.
|
|
382
|
+
*
|
|
383
|
+
* The combined `q=` and `ll=` form, because a coordinate positions the map and
|
|
384
|
+
* a NAME selects a place — `write.ts` records the same thing for the same
|
|
385
|
+
* reason.
|
|
386
|
+
*/
|
|
387
|
+
async openCard(place) {
|
|
388
|
+
this.#openUrl(`maps://?q=${encodeURIComponent(place.name)}&ll=${place.latitude},${place.longitude}`);
|
|
389
|
+
return await this.#poll((e) => e.id === CARD.add) !== null;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Is this place saved?
|
|
393
|
+
*
|
|
394
|
+
* Read off `AddButton`'s NAME, which is the state bit. `docs/desktop.md`
|
|
395
|
+
* corrected itself on this: `FavoriteButton` reports identical attributes
|
|
396
|
+
* either way, and the sibling carries `"Add"` versus `"Added"`.
|
|
397
|
+
*
|
|
398
|
+
* Localised, and that is a real limit rather than an oversight — there is no
|
|
399
|
+
* unlocalised state anywhere on this card.
|
|
400
|
+
*/
|
|
401
|
+
async isSaved() {
|
|
402
|
+
const add = await this.#poll((e) => e.id === CARD.add, this.#waits.sheet);
|
|
403
|
+
if (!add?.name) return null;
|
|
404
|
+
return add.name !== "Add";
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Open the card's overflow menu and return its items.
|
|
408
|
+
*
|
|
409
|
+
* POLLED, not settled. The first version asked for the items in the same
|
|
410
|
+
* breath as the press and got an empty list every time — the menu takes on the
|
|
411
|
+
* order of half a second to appear — and then reported "the overflow menu did
|
|
412
|
+
* not open", which is a sentence about Maps rather than about the race. That
|
|
413
|
+
* is this repo's own rule broken in the file that quotes it: **poll for the
|
|
414
|
+
* control, never wait a fixed time for it.**
|
|
415
|
+
*/
|
|
416
|
+
async #openMenu() {
|
|
417
|
+
const more = await this.#poll((e) => e.id === CARD.more);
|
|
418
|
+
if (!more) return [];
|
|
419
|
+
await this.#call("press", { handle: more.handle });
|
|
420
|
+
const deadline = Date.now() + this.#waits.sheet;
|
|
421
|
+
for (;;) {
|
|
422
|
+
const items = await this.#call("find_elements", {
|
|
423
|
+
bundleId: MAPS_BUNDLE,
|
|
424
|
+
role: "AXMenuItem"
|
|
425
|
+
});
|
|
426
|
+
if (items.elements?.length) return items.elements;
|
|
427
|
+
if (Date.now() >= deadline) return [];
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async #dismiss() {
|
|
432
|
+
await this.#call("key", {
|
|
433
|
+
key: "escape",
|
|
434
|
+
modifiers: []
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Save the open place into the Places library.
|
|
439
|
+
*
|
|
440
|
+
* Two presses, not one: `AddButton` raises a **"Name This Location"** sheet
|
|
441
|
+
* and nothing is written until its `Save` is pressed. The sheet REPLACES the
|
|
442
|
+
* window list — a full walk during it returns the sheet and nothing else — so
|
|
443
|
+
* the card's elements are unreachable until it closes, and `Save` carries no
|
|
444
|
+
* `AXIdentifier`, which is why it is addressed by name.
|
|
445
|
+
*/
|
|
446
|
+
async savePlace() {
|
|
447
|
+
const add = await this.#poll((e) => e.id === CARD.add);
|
|
448
|
+
if (!add) return false;
|
|
449
|
+
await this.#call("press", { handle: add.handle });
|
|
450
|
+
const save = await this.#poll((e) => e.role === "AXButton" && e.name === "Save", this.#waits.sheet);
|
|
451
|
+
if (!save) return false;
|
|
452
|
+
await this.#call("press", { handle: save.handle });
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Remove the open place from the Places library.
|
|
457
|
+
*
|
|
458
|
+
* The menu item is `delete_from_places`, and it is only present when the place
|
|
459
|
+
* IS saved — the same slot carries `add_to_places` when it is not. So its
|
|
460
|
+
* absence is a state reading rather than a failure, and this reports which.
|
|
461
|
+
*/
|
|
462
|
+
async removePlace() {
|
|
463
|
+
const items = await this.#openMenu();
|
|
464
|
+
if (!items.length) return "no-menu";
|
|
465
|
+
const remove = items.find((e) => e.id === CARD.deleteFromPlaces);
|
|
466
|
+
if (!remove) {
|
|
467
|
+
await this.#dismiss();
|
|
468
|
+
return items.some((e) => e.id === CARD.addToPlaces) ? "not-saved" : "no-menu";
|
|
469
|
+
}
|
|
470
|
+
await this.#call("press", { handle: remove.handle });
|
|
471
|
+
return "removed";
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Every guide Maps knows about, read off its own picker.
|
|
475
|
+
*
|
|
476
|
+
* This is the authoritative list, and it is not the same as the store's:
|
|
477
|
+
* `docs/desktop.md` found the picker showing a "Favorites" guide that
|
|
478
|
+
* `apple_maps_list_collections` does not return. Read here as a side effect of
|
|
479
|
+
* being on the way to a write, rather than as a tool of its own — reaching it
|
|
480
|
+
* needs a place card, which needs a `maps://` open, which leaves a Recents
|
|
481
|
+
* entry. That is too much side effect for something calling itself a read.
|
|
482
|
+
*
|
|
483
|
+
* Leaves the picker OPEN, because the caller is normally about to press a row.
|
|
484
|
+
*/
|
|
485
|
+
async openGuidePicker() {
|
|
486
|
+
const items = await this.#openMenu();
|
|
487
|
+
const guides = items.find((e) => e.id === CARD.addToGuides);
|
|
488
|
+
if (!guides) {
|
|
489
|
+
if (items.length) await this.#dismiss();
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
await this.#call("press", { handle: guides.handle });
|
|
493
|
+
if (!await this.#poll((e) => e.id === CARD.picker, this.#waits.sheet)) return null;
|
|
494
|
+
return (await this.#tree()).filter((e) => e.id === CARD.guideRow && e.name).map((e) => parseGuideRow(e.name));
|
|
495
|
+
}
|
|
496
|
+
/** Press one guide row, then Done. Returns false if no row matched. */
|
|
497
|
+
async chooseGuide(name) {
|
|
498
|
+
const wanted = name.toLowerCase();
|
|
499
|
+
const row = (await this.#tree()).find((e) => e.id === CARD.guideRow && parseGuideRow(e.name ?? "").name.toLowerCase() === wanted);
|
|
500
|
+
if (!row) return false;
|
|
501
|
+
await this.#call("press", { handle: row.handle });
|
|
502
|
+
const done = await this.#poll((e) => e.id === CARD.done, this.#waits.sheet);
|
|
503
|
+
if (!done) return false;
|
|
504
|
+
await this.#call("press", { handle: done.handle });
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Re-open the card and read the state bit, which is the only way to verify.
|
|
509
|
+
*
|
|
510
|
+
* **A write closes the card.** Saving through the naming sheet dismisses the
|
|
511
|
+
* whole place card and returns Maps to its main view, so reading `AddButton`
|
|
512
|
+
* straight after a press finds nothing and reports "the card does not say it
|
|
513
|
+
* is saved" — which is a sentence about the write having failed, when what
|
|
514
|
+
* actually happened is that the thing being read went away.
|
|
515
|
+
*
|
|
516
|
+
* So verification re-opens the place first. That costs another `maps://`,
|
|
517
|
+
* which is free in the sense that matters: the Recents entry this leaves was
|
|
518
|
+
* already left by opening the card in the first place.
|
|
519
|
+
*/
|
|
520
|
+
async verifySaved(place) {
|
|
521
|
+
if (!await this.openCard(place)) return null;
|
|
522
|
+
return this.isSaved();
|
|
523
|
+
}
|
|
524
|
+
/** Abandon the picker without filing anything. */
|
|
525
|
+
async cancelGuidePicker() {
|
|
526
|
+
const close = await this.#poll((e) => e.id === CARD.close, this.#waits.sheet);
|
|
527
|
+
if (close) await this.#call("press", { handle: close.handle });
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
//#endregion
|
|
241
531
|
//#region src/client/ref.ts
|
|
242
532
|
/**
|
|
243
533
|
* Refs for places and collections.
|
|
@@ -1038,6 +1328,10 @@ const openStore = (opts) => {
|
|
|
1038
1328
|
* Hence: never fabricate a place record, only ever copy one Maps wrote.
|
|
1039
1329
|
*/
|
|
1040
1330
|
const STORAGE_POLL_MS = 250;
|
|
1331
|
+
/** A pause that yields the event loop, unlike `Atomics.wait`, which does not. */
|
|
1332
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
1333
|
+
setTimeout(resolve, ms);
|
|
1334
|
+
});
|
|
1041
1335
|
/**
|
|
1042
1336
|
* Progress, on stderr.
|
|
1043
1337
|
*
|
|
@@ -1221,7 +1515,17 @@ var MapsWriter = class {
|
|
|
1221
1515
|
* poll with a short-lived read-only handle each time.
|
|
1222
1516
|
* 3. WRITE — open read-write, insert, close.
|
|
1223
1517
|
*/
|
|
1224
|
-
|
|
1518
|
+
/**
|
|
1519
|
+
* Async because the seed can take THIRTY SECONDS.
|
|
1520
|
+
*
|
|
1521
|
+
* When the place is not already in the store this asks Maps to mint it
|
|
1522
|
+
* through the URL scheme and then polls for the row to appear. That poll used
|
|
1523
|
+
* `Atomics.wait`, which blocks the thread — and this server is single
|
|
1524
|
+
* threaded, so for the whole seed it answered nothing at all: not a ping, not
|
|
1525
|
+
* a cancellation, not another tool call. A sleep that yields costs the same
|
|
1526
|
+
* wall clock and leaves the server able to speak.
|
|
1527
|
+
*/
|
|
1528
|
+
async addFavorite(input) {
|
|
1225
1529
|
const hasCoords = input.latitude !== void 0 && input.longitude !== void 0;
|
|
1226
1530
|
const lat = input.latitude ?? 0;
|
|
1227
1531
|
const lon = input.longitude ?? 0;
|
|
@@ -1264,7 +1568,7 @@ var MapsWriter = class {
|
|
|
1264
1568
|
} finally {
|
|
1265
1569
|
probe.close();
|
|
1266
1570
|
}
|
|
1267
|
-
if (!donor)
|
|
1571
|
+
if (!donor) await sleep(STORAGE_POLL_MS);
|
|
1268
1572
|
polls += 1;
|
|
1269
1573
|
if (polls % 8 === 0) progress(`still waiting (${polls * (STORAGE_POLL_MS / 1e3)}s)…`);
|
|
1270
1574
|
}
|
|
@@ -1294,22 +1598,23 @@ var MapsWriter = class {
|
|
|
1294
1598
|
progress("writing the favourite");
|
|
1295
1599
|
const db = this.#open();
|
|
1296
1600
|
try {
|
|
1297
|
-
const favEnt = this.#entity(db, "FavoriteItem");
|
|
1298
|
-
const mixEnt = this.#entity(db, "MixinMapItem");
|
|
1299
|
-
const favPk = this.#nextPk(db, "FavoriteItem", "ZFAVORITEITEM");
|
|
1300
|
-
const mixPk = this.#nextPk(db, "MixinMapItem", "ZMIXINMAPITEM");
|
|
1301
1601
|
const now = Date.now() / 1e3 - CORE_DATA_EPOCH_OFFSET;
|
|
1302
1602
|
const id = uuidBytes();
|
|
1303
1603
|
const label = input.name ?? donor.name ?? input.query;
|
|
1304
|
-
|
|
1604
|
+
let favPk = 0;
|
|
1305
1605
|
db.exec("BEGIN IMMEDIATE");
|
|
1306
1606
|
try {
|
|
1607
|
+
const favEnt = this.#entity(db, "FavoriteItem");
|
|
1608
|
+
const mixEnt = this.#entity(db, "MixinMapItem");
|
|
1609
|
+
favPk = this.#nextPk(db, "FavoriteItem", "ZFAVORITEITEM");
|
|
1610
|
+
const mixPk = this.#nextPk(db, "MixinMapItem", "ZMIXINMAPITEM");
|
|
1611
|
+
const position = Number(db.prepare(`SELECT COUNT(*) AS c FROM ZFAVORITEITEM`).get().c);
|
|
1307
1612
|
db.prepare(`INSERT INTO "ZFAVORITEITEM"
|
|
1308
1613
|
(Z_PK, Z_ENT, Z_OPT, ZHIDDEN, ZPOSITIONINDEX, ZSOURCE, ZTYPE, ZVERSION,
|
|
1309
1614
|
ZMAPITEM, ZMUID, ZCREATETIME, ZMODIFICATIONTIME, ZMAPITEMLASTREFRESHED,
|
|
1310
1615
|
ZLATITUDE, ZLONGITUDE, ZMAPITEMNAME, ZMAPITEMADDRESS, ZMAPITEMCATEGORY,
|
|
1311
1616
|
ZIDENTIFIER)
|
|
1312
|
-
VALUES (?, ?, 1, 0, ?, 0, 1, 2, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(favPk, favEnt, position, mixPk, donor.muid === null ? null :
|
|
1617
|
+
VALUES (?, ?, 1, 0, ?, 0, 1, 2, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(favPk, favEnt, position, mixPk, donor.muid === null ? null : BigInt(donor.muid), now, now, now, donor.lat, donor.lon, label, donor.address, donor.category, id);
|
|
1313
1618
|
db.prepare(`INSERT INTO "ZMIXINMAPITEM"
|
|
1314
1619
|
(Z_PK, Z_ENT, Z_OPT, ZFAVORITEITEM, ZCREATETIME, ZMODIFICATIONTIME,
|
|
1315
1620
|
ZLATITUDE, ZLONGITUDE, ZMAPITEMSTORAGE)
|
|
@@ -1381,6 +1686,14 @@ const summariseEntity = (e) => ({
|
|
|
1381
1686
|
});
|
|
1382
1687
|
var AppleMapsClient = class {
|
|
1383
1688
|
#config;
|
|
1689
|
+
/**
|
|
1690
|
+
* The interface lane, when this server is hosted by Cupertino.
|
|
1691
|
+
*
|
|
1692
|
+
* Null when it is not, which is the supported case rather than a fault: the
|
|
1693
|
+
* npm package has to work with no app on the machine. `tools/index.ts` skips
|
|
1694
|
+
* registering the tools that need it.
|
|
1695
|
+
*/
|
|
1696
|
+
#ax;
|
|
1384
1697
|
#logger;
|
|
1385
1698
|
#home;
|
|
1386
1699
|
#located = null;
|
|
@@ -1388,12 +1701,16 @@ var AppleMapsClient = class {
|
|
|
1388
1701
|
#storeError = null;
|
|
1389
1702
|
constructor(opts) {
|
|
1390
1703
|
this.#config = opts.config;
|
|
1704
|
+
this.#ax = opts.ax !== void 0 ? opts.ax : MapsAxLane.open();
|
|
1391
1705
|
this.#logger = opts.logger;
|
|
1392
1706
|
this.#home = opts.home;
|
|
1393
1707
|
}
|
|
1394
1708
|
get config() {
|
|
1395
1709
|
return this.#config;
|
|
1396
1710
|
}
|
|
1711
|
+
get axLane() {
|
|
1712
|
+
return this.#ax;
|
|
1713
|
+
}
|
|
1397
1714
|
located() {
|
|
1398
1715
|
this.#located ??= locateStore({
|
|
1399
1716
|
storePath: this.#config.storePath,
|
|
@@ -1748,7 +2065,13 @@ const buildDiagnostics = async (client) => {
|
|
|
1748
2065
|
working: status.store.opened
|
|
1749
2066
|
},
|
|
1750
2067
|
appleEvents: "none — Maps is not scriptable",
|
|
1751
|
-
writes:
|
|
2068
|
+
writes: {
|
|
2069
|
+
enabled: client.config.allowWrites,
|
|
2070
|
+
lane: "SQL, directly into the Core Data store. Maps ships no scripting dictionary and registers no App Intents on macOS, so there is no lane where the app performs the write on our behalf — this is the only surface here that writes its own store.",
|
|
2071
|
+
seeding: "A place is only real to Maps if it carries a ZMAPITEMSTORAGE blob, which this repo cannot generate. Opening maps://?q=<name>&ll=<lat>,<lon> through LaunchServices makes Maps mint one, and that record is copied. The place is therefore left in Recents whether or not the favourite is kept, and resolving it can take tens of seconds.",
|
|
2072
|
+
blastRadius: "The store is mirrored by NSPersistentCloudKitContainer, and mirroring does not wait to be told. A malformed row is not a local mistake: it reaches every device on the account as soon as Maps next runs. Hence the rule in client/write.ts — never fabricate a place record, only ever copy one Maps wrote.",
|
|
2073
|
+
tools: client.config.allowWrites ? ["apple_maps_add_favorite", "apple_maps_remove_favorite"] : []
|
|
2074
|
+
}
|
|
1752
2075
|
},
|
|
1753
2076
|
store: {
|
|
1754
2077
|
path: located.storePath,
|
|
@@ -1786,6 +2109,138 @@ const registerDiagnosticsTools = (server, client) => {
|
|
|
1786
2109
|
}, async () => wrap(() => buildDiagnostics(client)));
|
|
1787
2110
|
};
|
|
1788
2111
|
//#endregion
|
|
2112
|
+
//#region src/tools/ax-writes.ts
|
|
2113
|
+
/**
|
|
2114
|
+
* The tools that go through Maps' own interface rather than its store.
|
|
2115
|
+
*
|
|
2116
|
+
* ## Why these are not in `writes.ts`
|
|
2117
|
+
*
|
|
2118
|
+
* Different lane, different objects, different failure modes. `writes.ts` writes
|
|
2119
|
+
* SQL into the Core Data store and reaches **favourites**; these press controls
|
|
2120
|
+
* on a place card and reach the **Places library** and **guide membership** —
|
|
2121
|
+
* two sets the SQL lane does not offer at all. `docs/maps.md` lists guide
|
|
2122
|
+
* membership as unbuilt for exactly that reason.
|
|
2123
|
+
*
|
|
2124
|
+
* ## Registered only when the app is hosting this server
|
|
2125
|
+
*
|
|
2126
|
+
* The Accessibility grant belongs to Cupertino.app, not to node, so a package
|
|
2127
|
+
* installed from npm and run by hand has no lane here and these tools are not
|
|
2128
|
+
* registered at all — the same rule the write gate follows, so a host is never
|
|
2129
|
+
* told about a tool that cannot work for it.
|
|
2130
|
+
*
|
|
2131
|
+
* ## What every description has to say
|
|
2132
|
+
*
|
|
2133
|
+
* **It opens Maps and leaves the place in Recents.** Reaching a card means
|
|
2134
|
+
* opening `maps://`, which is how the SQL lane seeds a record too. There is no
|
|
2135
|
+
* way to have the card without the Recents entry.
|
|
2136
|
+
*
|
|
2137
|
+
* **It moves the user's screen.** Unlike a SQL write, this brings a window
|
|
2138
|
+
* forward and presses things in it. That is worth stating plainly.
|
|
2139
|
+
*/
|
|
2140
|
+
const noCard = (query, disturbed) => fail(`Maps did not show a place card for "${query}" within 10s. Nothing was changed. The coordinates may not resolve to a place Maps recognises, or Maps may not have finished loading it.` + disturbed);
|
|
2141
|
+
const registerAxWriteTools = (server, lane) => {
|
|
2142
|
+
const place = {
|
|
2143
|
+
query: z.string().min(1).describe("The place's name, as you would type it into Maps' search field."),
|
|
2144
|
+
latitude: z.number().min(-90).max(90).describe("Latitude of the place."),
|
|
2145
|
+
longitude: z.number().min(-180).max(180).describe("Longitude of the place.")
|
|
2146
|
+
};
|
|
2147
|
+
const openCard = async (args) => lane.openCard({
|
|
2148
|
+
name: args.query,
|
|
2149
|
+
latitude: args.latitude,
|
|
2150
|
+
longitude: args.longitude
|
|
2151
|
+
});
|
|
2152
|
+
server.registerTool("apple_maps_save_place", {
|
|
2153
|
+
description: "Save a place into Maps' Places library, by pressing Add on its card. This is NOT a favourite: the Pinned list is a different set, written by apple_maps_add_favorite. Maps performs the write itself, so it cannot produce a malformed record. SIDE EFFECTS: opens Maps, brings its window forward, and leaves the place in the user's Recents. Syncs to the user's other Apple devices. Needs Accessibility for Cupertino.",
|
|
2154
|
+
inputSchema: place,
|
|
2155
|
+
annotations: {
|
|
2156
|
+
readOnlyHint: false,
|
|
2157
|
+
destructiveHint: false,
|
|
2158
|
+
idempotentHint: true
|
|
2159
|
+
}
|
|
2160
|
+
}, async (args) => wrapResult(async () => {
|
|
2161
|
+
const watch = lane.watch();
|
|
2162
|
+
const disturbed = async () => interferenceNote(await watch.check());
|
|
2163
|
+
if (!await openCard(args)) return noCard(args.query, await disturbed());
|
|
2164
|
+
if (await lane.isSaved() === true) return ok({
|
|
2165
|
+
saved: true,
|
|
2166
|
+
alreadySaved: true,
|
|
2167
|
+
query: args.query
|
|
2168
|
+
});
|
|
2169
|
+
if (!await lane.savePlace()) return fail(`The card for "${args.query}" opened but the naming sheet did not, so nothing was saved. The card may still be on screen.` + await disturbed());
|
|
2170
|
+
return await lane.verifySaved({
|
|
2171
|
+
name: args.query,
|
|
2172
|
+
latitude: args.latitude,
|
|
2173
|
+
longitude: args.longitude
|
|
2174
|
+
}) === true ? ok({
|
|
2175
|
+
saved: true,
|
|
2176
|
+
alreadySaved: false,
|
|
2177
|
+
query: args.query
|
|
2178
|
+
}) : fail(`"${args.query}" was pressed through the save sheet but its card does not report it as saved, so this MUST NOT be reported as done. Check Maps.` + await disturbed());
|
|
2179
|
+
}));
|
|
2180
|
+
server.registerTool("apple_maps_remove_saved_place", {
|
|
2181
|
+
description: "Remove a place from Maps' Places library, through the card's overflow menu. This does NOT touch favourites — use apple_maps_remove_favorite for those. SIDE EFFECTS: opens Maps, brings its window forward, and leaves the place in the user's Recents. Syncs to the user's other Apple devices. Needs Accessibility for Cupertino.",
|
|
2182
|
+
inputSchema: place,
|
|
2183
|
+
annotations: {
|
|
2184
|
+
readOnlyHint: false,
|
|
2185
|
+
destructiveHint: true,
|
|
2186
|
+
idempotentHint: true
|
|
2187
|
+
}
|
|
2188
|
+
}, async (args) => wrapResult(async () => {
|
|
2189
|
+
const watch = lane.watch();
|
|
2190
|
+
const disturbed = async () => interferenceNote(await watch.check());
|
|
2191
|
+
if (!await openCard(args)) return noCard(args.query, await disturbed());
|
|
2192
|
+
const outcome = await lane.removePlace();
|
|
2193
|
+
if (outcome === "not-saved") return ok({
|
|
2194
|
+
removed: false,
|
|
2195
|
+
wasSaved: false,
|
|
2196
|
+
query: args.query
|
|
2197
|
+
});
|
|
2198
|
+
if (outcome === "no-menu") return fail(`The card for "${args.query}" opened but its overflow menu did not, so nothing was removed.` + await disturbed());
|
|
2199
|
+
return await lane.verifySaved({
|
|
2200
|
+
name: args.query,
|
|
2201
|
+
latitude: args.latitude,
|
|
2202
|
+
longitude: args.longitude
|
|
2203
|
+
}) === false ? ok({
|
|
2204
|
+
removed: true,
|
|
2205
|
+
wasSaved: true,
|
|
2206
|
+
query: args.query
|
|
2207
|
+
}) : fail(`"${args.query}" was pressed through Delete from Places but its card still reports it as saved, so this MUST NOT be reported as done. Check Maps.` + await disturbed());
|
|
2208
|
+
}));
|
|
2209
|
+
server.registerTool("apple_maps_add_place_to_guide", {
|
|
2210
|
+
description: "File a place into one of Maps' Guides, through the card's Add to Guides picker. This is the write apple_maps_list_collections has no counterpart for. IT CANNOT CONFIRM ITSELF: Maps does not expose whether the place ended up in the guide, so the result says filed: \"unverified\" and it must not be reported as done. The guide must already exist; when the name does not match, the refusal lists every guide Maps offers, which is the authoritative list and can differ from what apple_maps_list_collections returns. SIDE EFFECTS: opens Maps, brings its window forward, and leaves the place in the user's Recents. Syncs to the user's other Apple devices. Needs Accessibility for Cupertino.",
|
|
2211
|
+
inputSchema: {
|
|
2212
|
+
...place,
|
|
2213
|
+
guide: z.string().min(1).describe("Name of the guide to file it into, as Maps shows it.")
|
|
2214
|
+
},
|
|
2215
|
+
annotations: {
|
|
2216
|
+
readOnlyHint: false,
|
|
2217
|
+
destructiveHint: false,
|
|
2218
|
+
idempotentHint: true
|
|
2219
|
+
}
|
|
2220
|
+
}, async (args) => wrapResult(async () => {
|
|
2221
|
+
const watch = lane.watch();
|
|
2222
|
+
const disturbed = async () => interferenceNote(await watch.check());
|
|
2223
|
+
if (!await openCard(args)) return noCard(args.query, await disturbed());
|
|
2224
|
+
const guides = await lane.openGuidePicker();
|
|
2225
|
+
if (guides === null) return fail(`The card for "${args.query}" opened but its Add to Guides picker did not, so nothing was filed.` + await disturbed());
|
|
2226
|
+
if (!await lane.chooseGuide(args.guide)) {
|
|
2227
|
+
await lane.cancelGuidePicker();
|
|
2228
|
+
return fail(`Maps offers no guide called "${args.guide}", so nothing was filed. It offers: ${guides.map((g) => g.name).join(", ")}.`, { guides: compactGuides(guides) });
|
|
2229
|
+
}
|
|
2230
|
+
return ok({
|
|
2231
|
+
filed: "unverified",
|
|
2232
|
+
query: args.query,
|
|
2233
|
+
guide: args.guide,
|
|
2234
|
+
guides: compactGuides(guides),
|
|
2235
|
+
note: "The guide row and then Done were pressed. Maps does not expose whether the place ended up in the guide — the picker's counts do not refresh in the accessibility tree — so this MUST NOT be reported to the user as filed. Check Maps."
|
|
2236
|
+
});
|
|
2237
|
+
}));
|
|
2238
|
+
};
|
|
2239
|
+
const compactGuides = (guides) => guides.map((g) => g.places === null ? { name: g.name } : {
|
|
2240
|
+
name: g.name,
|
|
2241
|
+
places: g.places
|
|
2242
|
+
});
|
|
2243
|
+
//#endregion
|
|
1789
2244
|
//#region src/tools/places.ts
|
|
1790
2245
|
/**
|
|
1791
2246
|
* The place tools.
|
|
@@ -1863,10 +2318,11 @@ const registerPlaceTools = (server, client) => {
|
|
|
1863
2318
|
}
|
|
1864
2319
|
}, async ({ ref, limit }) => wrapResult(async () => {
|
|
1865
2320
|
const collectionId = client.collectionRowId(decodeCollectionRef(ref));
|
|
2321
|
+
if (collectionId === null) return fail(`No collection matches ${ref}. Refs come from apple_maps_list_collections and go stale when a guide is deleted or renamed away.`);
|
|
1866
2322
|
const capped = resolveLimit(limit, client.config.maxResults);
|
|
1867
2323
|
const result = client.places("collection-item", {
|
|
1868
2324
|
limit: capped,
|
|
1869
|
-
collectionId
|
|
2325
|
+
collectionId
|
|
1870
2326
|
});
|
|
1871
2327
|
if (!client.collections({ limit: 1e3 }).itemsEnumerable) return fail("This store does not expose which collection an item belongs to, so its places cannot be listed. The collection itself and its place count are still readable through apple_maps_list_collections.");
|
|
1872
2328
|
return ok(compact({
|
|
@@ -1988,7 +2444,7 @@ const registerWriteTools = (server, client) => {
|
|
|
1988
2444
|
idempotentHint: true
|
|
1989
2445
|
}
|
|
1990
2446
|
}, async ({ query, latitude, longitude, name }) => wrapResult(async () => {
|
|
1991
|
-
const result = client.writer().addFavorite({
|
|
2447
|
+
const result = await client.writer().addFavorite({
|
|
1992
2448
|
query,
|
|
1993
2449
|
latitude,
|
|
1994
2450
|
longitude,
|
|
@@ -2041,6 +2497,8 @@ const registerTools = (server, client, ctx) => {
|
|
|
2041
2497
|
registerPlaceTools(server, client);
|
|
2042
2498
|
if (!ctx.allowWrites) return;
|
|
2043
2499
|
registerWriteTools(server, client);
|
|
2500
|
+
const ax = client.axLane;
|
|
2501
|
+
if (ax) registerAxWriteTools(server, ax);
|
|
2044
2502
|
};
|
|
2045
2503
|
//#endregion
|
|
2046
2504
|
//#region src/server.ts
|
|
@@ -2089,4 +2547,4 @@ const createServer = (opts) => {
|
|
|
2089
2547
|
//#endregion
|
|
2090
2548
|
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 };
|
|
2091
2549
|
|
|
2092
|
-
//# sourceMappingURL=server-
|
|
2550
|
+
//# sourceMappingURL=server-C_leu2E0.js.map
|