@mgcrea/mcp-apple-core 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/dist/index.js CHANGED
@@ -1,9 +1,327 @@
1
+ import { connect } from "node:net";
1
2
  import { accessSync, constants, readFileSync, statSync } from "node:fs";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import { z } from "zod";
4
5
  import { execFile } from "node:child_process";
5
6
  import { createHash } from "node:crypto";
6
7
  import { DatabaseSync } from "node:sqlite";
8
+ //#region src/errors.ts
9
+ /** Base class so `toFailure` can carry structured detail through in one branch. */
10
+ var AppleAutomationError = class extends Error {
11
+ name = "AppleAutomationError";
12
+ details;
13
+ constructor(message, details) {
14
+ super(message);
15
+ this.details = details;
16
+ }
17
+ };
18
+ /** The host process may not send Apple Events to the app (osascript -1743). */
19
+ var TccDeniedError = class extends AppleAutomationError {
20
+ name = "TccDeniedError";
21
+ constructor(surface) {
22
+ super(`Not authorized to control ${surface.appName}. Grant it in System Settings > Privacy & Security > Automation > (the app running this server) > ${surface.appName}, then restart the server. If no entry appears, the first attempt was denied before the prompt could be answered — run \`tccutil reset AppleEvents\` and try again.`);
23
+ }
24
+ };
25
+ /** The app is not running and the operation refuses to launch it. */
26
+ var AppNotRunningError = class extends AppleAutomationError {
27
+ name = "AppNotRunningError";
28
+ constructor(surface) {
29
+ super(`${surface.appName} is not running. Read tools do not launch it, because launching it steals focus and can start a sync. Open ${surface.appName} and retry.`);
30
+ }
31
+ };
32
+ /** The app was busy and refused the Apple Event (-1712). Retried once before surfacing. */
33
+ var AppBusyError = class extends AppleAutomationError {
34
+ name = "AppBusyError";
35
+ constructor(surface) {
36
+ super(`${surface.appName} is busy (probably syncing) and did not answer in time. Retry in a few seconds.`);
37
+ }
38
+ };
39
+ /** osascript exceeded its budget and was killed. */
40
+ var OsascriptTimeoutError = class extends AppleAutomationError {
41
+ name = "OsascriptTimeoutError";
42
+ constructor(timeoutMs, surface) {
43
+ super(`${surface.appName} did not answer within ${timeoutMs}ms. It may be mid-sync, or a permission prompt may be waiting on screen. Raise ${surface.envPrefix}_OSASCRIPT_TIMEOUT_MS if this is routine at your data size.`);
44
+ }
45
+ };
46
+ /**
47
+ * A write was attempted while writes are disabled. Under the house pattern write
48
+ * tools are not registered at all when `allowWrites` is off, so this is a
49
+ * belt-and-braces guard for the library surface, not a path tools can reach.
50
+ */
51
+ var WritesDisabledError = class extends AppleAutomationError {
52
+ name = "WritesDisabledError";
53
+ constructor(surface) {
54
+ super(`Writes are disabled. Set ${surface.envPrefix}_ALLOW_WRITES=1 to enable the mutating tools.`);
55
+ }
56
+ };
57
+ /** A read-only index could not be opened, so the file lane is unavailable. */
58
+ var IndexUnavailableError = class extends AppleAutomationError {
59
+ name = "IndexUnavailableError";
60
+ };
61
+ /** The store's schema is not the one we know how to read. */
62
+ var SchemaDriftError = class extends AppleAutomationError {
63
+ name = "SchemaDriftError";
64
+ };
65
+ /** The server is not running on macOS, or osascript is missing. */
66
+ var PlatformError = class extends AppleAutomationError {
67
+ name = "PlatformError";
68
+ };
69
+ /** osascript exited 0 but did not produce the JSON envelope we require. */
70
+ var ProtocolError = class extends AppleAutomationError {
71
+ name = "ProtocolError";
72
+ };
73
+ /** A local precondition failed before anything was sent to the app. */
74
+ var PreconditionError = class extends AppleAutomationError {
75
+ name = "PreconditionError";
76
+ };
77
+ /**
78
+ * A date argument could not be read.
79
+ *
80
+ * Refusing is the whole point. The alternative — which is what Mail, Messages
81
+ * and Notes did — is `Date.parse` returning `NaN`, node:sqlite binding it as
82
+ * `NULL`, and the query matching nothing: an empty result that looks like an
83
+ * answer. The message lists the accepted forms because the caller is usually a
84
+ * model that guessed, and the fix is to show it the grammar.
85
+ */
86
+ var InvalidDateError = class extends AppleAutomationError {
87
+ name = "InvalidDateError";
88
+ constructor(field, raw, reason) {
89
+ super(`Could not read ${field} from ${JSON.stringify(raw)}: ${reason}. Accepted: an ISO-8601 date "2026-08-20" (a whole day, local) or date-time "2026-08-20T09:00" (local unless it carries an offset), a signed offset like "+2d", "-3h", "+45m", "+1w", "today", "yesterday", "tomorrow 09:00", "next monday" or "last friday".`, {
90
+ field,
91
+ raw
92
+ });
93
+ }
94
+ };
95
+ //#endregion
96
+ //#region src/ax.ts
97
+ /**
98
+ * Borrowing the app's Accessibility driver.
99
+ *
100
+ * ## Why this exists at all
101
+ *
102
+ * `AXUIElementCopyAttributeValue` reads an attribute in **0.202 ms**. Reaching
103
+ * the same attribute through `osascript` + JXA + System Events costs **47.4 ms**
104
+ * — a 234x difference that is entirely transport, measured both ways on one
105
+ * machine in `docs/desktop.md`. Every Accessibility number this repo recorded
106
+ * before that document was taken through System Events, which is why the lane
107
+ * was closed three times on figures that were pricing the wrong thing.
108
+ *
109
+ * The native call cannot be made from here. Node has no binding for it, and
110
+ * more importantly the grant does not belong to node: TCC attaches
111
+ * Accessibility to the **responsible GUI ancestor**, so it is Cupertino.app
112
+ * that holds it. Hence a channel rather than a port — the app already runs the
113
+ * driver, and `ServerHost` will lend it for one bundle id.
114
+ *
115
+ * ## The second grant this removes, which matters more than the speed
116
+ *
117
+ * Driving a UI through System Events needs Automation-to-System-Events **on top
118
+ * of** Accessibility. Two grants, given in two different System Settings panes,
119
+ * to reach one window. The native driver needs the first and not the second.
120
+ *
121
+ * ## Absence is the fallback, not an error
122
+ *
123
+ * `CUPERTINO_AX_SOCKET` and `CUPERTINO_AX_FOR` are set by `ServerLocator` and by
124
+ * nothing else. A package installed from npm and run by hand has neither, and
125
+ * that is the supported case: these are published artifacts that must work with
126
+ * no app on the machine. `open()` returns null there, and the caller keeps
127
+ * whatever lane it had. It never throws to say "no app".
128
+ *
129
+ * ## Lazy, deliberately
130
+ *
131
+ * Nothing connects at construction. `SurfaceCatalog` probes servers to learn
132
+ * what they register, and a probe that opened a socket would make every capability
133
+ * scan depend on the host being ready to answer one.
134
+ */
135
+ /** The handshake `ServerHost` expects, and the reply it sends back. */
136
+ const PROTOCOL = "cupertino/1";
137
+ const LENT_SURFACE = "desktop";
138
+ const OK = "ok";
139
+ /**
140
+ * How long one call may take.
141
+ *
142
+ * A walk carries its own five-second budget and the AX messaging timeout is two
143
+ * seconds per element, so the ceiling that matters is the app's, not this one's.
144
+ * This is only here so a host that stopped answering fails rather than hanging a
145
+ * tool call forever.
146
+ */
147
+ const CALL_TIMEOUT_MS = 3e4;
148
+ /**
149
+ * The app is there and said no, or stopped answering.
150
+ *
151
+ * Distinct from the null `openAxChannel` returns, which means there is no app
152
+ * to ask — see the note there on why those two must not collapse.
153
+ */
154
+ var AxChannelError = class extends AppleAutomationError {
155
+ name = "AxChannelError";
156
+ constructor(surface, message) {
157
+ super(`${surface.appName}: ${message}`, { surface: surface.appName });
158
+ }
159
+ };
160
+ /** What the host told us, when it refused the handshake. */
161
+ const refusal = (line) => line.startsWith("err ") ? line.slice(4) : `unexpected handshake reply '${line}'`;
162
+ /**
163
+ * Open the channel, or return null when this server is not hosted by the app.
164
+ *
165
+ * Null and a throw are different answers and the difference is the whole
166
+ * contract: null means "there is no app here, use your other lane", a throw
167
+ * means "there is an app and it said no", which a caller must report rather
168
+ * than paper over.
169
+ */
170
+ const openAxChannel = (surface, env = process.env) => {
171
+ const socketPath = env.CUPERTINO_AX_SOCKET?.trim();
172
+ const identity = env.CUPERTINO_AX_FOR?.trim();
173
+ if (!socketPath || !identity) return null;
174
+ let socket = null;
175
+ let pending = Promise.resolve();
176
+ let nextId = 1;
177
+ const connectOnce = () => new Promise((resolve, reject) => {
178
+ const s = connect(socketPath);
179
+ let buffer = "";
180
+ const onError = (error) => {
181
+ s.destroy();
182
+ reject(new AxChannelError(surface, `cannot reach Cupertino: ${error.message}`));
183
+ };
184
+ s.once("error", onError);
185
+ s.setEncoding("utf8");
186
+ const onData = (chunk) => {
187
+ buffer += chunk;
188
+ const newline = buffer.indexOf("\n");
189
+ if (newline < 0) return;
190
+ const line = buffer.slice(0, newline).trim();
191
+ s.off("data", onData);
192
+ s.off("error", onError);
193
+ if (line !== OK) {
194
+ s.destroy();
195
+ reject(new AxChannelError(surface, refusal(line)));
196
+ return;
197
+ }
198
+ resolve(s);
199
+ };
200
+ s.on("data", onData);
201
+ s.write(`${PROTOCOL} ${LENT_SURFACE} for=${identity}\n`);
202
+ });
203
+ const request = async (payload) => {
204
+ socket ??= await connectOnce();
205
+ const live = socket;
206
+ const id = nextId++;
207
+ const line = `${JSON.stringify({
208
+ jsonrpc: "2.0",
209
+ id,
210
+ method: "tools/call",
211
+ params: {
212
+ name: payload.tool,
213
+ arguments: payload.args
214
+ }
215
+ })}\n`;
216
+ return new Promise((resolve, reject) => {
217
+ let buffer = "";
218
+ const timer = setTimeout(() => {
219
+ cleanup();
220
+ reject(new AxChannelError(surface, `${payload.tool} did not answer in ${CALL_TIMEOUT_MS}ms`));
221
+ }, CALL_TIMEOUT_MS);
222
+ const cleanup = () => {
223
+ clearTimeout(timer);
224
+ live.off("data", onData);
225
+ live.off("error", onError);
226
+ live.off("close", onClose);
227
+ };
228
+ const onError = (error) => {
229
+ cleanup();
230
+ reject(new AxChannelError(surface, `${payload.tool} failed: ${error.message}`));
231
+ };
232
+ const onClose = () => {
233
+ cleanup();
234
+ socket = null;
235
+ reject(new AxChannelError(surface, `Cupertino closed the connection during ${payload.tool}`));
236
+ };
237
+ const onData = (chunk) => {
238
+ buffer += chunk;
239
+ const newline = buffer.indexOf("\n");
240
+ if (newline < 0) return;
241
+ const raw = buffer.slice(0, newline);
242
+ cleanup();
243
+ let message;
244
+ try {
245
+ message = JSON.parse(raw);
246
+ } catch {
247
+ reject(new AxChannelError(surface, `unparseable reply to ${payload.tool}`));
248
+ return;
249
+ }
250
+ if (message.error) {
251
+ reject(new AxChannelError(surface, message.error.message ?? "unknown error"));
252
+ return;
253
+ }
254
+ const text = message.result?.content?.[0]?.text;
255
+ if (text === void 0) {
256
+ reject(new AxChannelError(surface, `empty reply to ${payload.tool}`));
257
+ return;
258
+ }
259
+ if (message.result?.isError === true) {
260
+ reject(new AxChannelError(surface, `${payload.tool}: ${text}`));
261
+ return;
262
+ }
263
+ try {
264
+ resolve(JSON.parse(text));
265
+ } catch {
266
+ resolve(text);
267
+ }
268
+ };
269
+ live.on("data", onData);
270
+ live.once("error", onError);
271
+ live.once("close", onClose);
272
+ live.write(line);
273
+ });
274
+ };
275
+ return {
276
+ call(payload) {
277
+ const result = pending.then(() => request(payload));
278
+ pending = result.then(() => void 0, () => void 0);
279
+ return result;
280
+ },
281
+ close() {
282
+ socket?.destroy();
283
+ socket = null;
284
+ }
285
+ };
286
+ };
287
+ /**
288
+ * Start watching. `check()` answers for the span since this call.
289
+ *
290
+ * Returns null from `check()` when the question could not be asked at all,
291
+ * which is not the same as "undisturbed" and must not be reported as it.
292
+ */
293
+ const watchInterference = (channel) => {
294
+ const started = Date.now();
295
+ return { async check() {
296
+ const elapsedSeconds = (Date.now() - started) / 1e3;
297
+ try {
298
+ const secondsSinceInput = (await channel.call({
299
+ tool: "apple_desktop_user_activity",
300
+ args: {}
301
+ })).secondsSinceInput;
302
+ if (typeof secondsSinceInput !== "number") return null;
303
+ return {
304
+ disturbed: secondsSinceInput < elapsedSeconds,
305
+ secondsSinceInput,
306
+ elapsedSeconds
307
+ };
308
+ } catch {
309
+ return null;
310
+ }
311
+ } };
312
+ };
313
+ /**
314
+ * The sentence to append to a failure, or "" when nothing useful can be said.
315
+ *
316
+ * Deliberately says nothing when the machine was quiet: a failure that reads
317
+ * "nobody touched it" invites the reader to stop looking, and the point of this
318
+ * is to send them to the RIGHT place rather than to reassure them.
319
+ */
320
+ const interferenceNote = (found) => {
321
+ if (!found?.disturbed) return "";
322
+ return ` Someone used this Mac ${found.secondsSinceInput.toFixed(1)}s ago, during the ${found.elapsedSeconds.toFixed(1)}s this took — a keystroke or click lands wherever the focus is, so that alone can explain this. Retry with the machine idle before looking further.`;
323
+ };
324
+ //#endregion
7
325
  //#region src/build-info.ts
8
326
  /**
9
327
  * Read a package's own name and version at startup, so they are always accurate
@@ -521,74 +839,216 @@ const parseConfig = (schema, raw) => {
521
839
  return parsed.data;
522
840
  };
523
841
  //#endregion
524
- //#region src/errors.ts
525
- /** Base class so `toFailure` can carry structured detail through in one branch. */
526
- var AppleAutomationError = class extends Error {
527
- name = "AppleAutomationError";
528
- details;
529
- constructor(message, details) {
530
- super(message);
531
- this.details = details;
532
- }
842
+ //#region src/dates.ts
843
+ /**
844
+ * The one grammar every surface reads its date arguments with.
845
+ *
846
+ * ## Why it is here
847
+ *
848
+ * This shipped as three copies. Reminders wrote it, Calendar repeated it
849
+ * verbatim, and Safari repeated it again with signed offsets and `yesterday`
850
+ * added — that third copy's header says outright that three is the trigger for
851
+ * hoisting and that the copies had already diverged. Meanwhile Mail, Messages
852
+ * and Notes never got a grammar at all: they called `Date.parse` or
853
+ * `new Date()` directly, which is where the bugs were.
854
+ *
855
+ * They were not small bugs, and they were all the same shape — a date argument
856
+ * that produced a confident, plausible, wrong answer:
857
+ *
858
+ * - `Date.parse("last week")` is `NaN`, node:sqlite binds `NaN` as `NULL`,
859
+ * and `date_received >= NULL` matches nothing. A model that wrote a date in
860
+ * words got zero results and no error.
861
+ * - `new Date("2026-08-20")` is UTC midnight but `new Date("2026-08-20T00:00")`
862
+ * is LOCAL midnight, so two spellings a tool description treated as the
863
+ * same thing bounded a query an hour or five apart.
864
+ * - `"2026-02-30T09:00"` silently became 2 March, because the rollover guard
865
+ * existed on the date branch and not on the date-time one.
866
+ *
867
+ * ## The rule
868
+ *
869
+ * 1. `YYYY-MM-DD` names a **local calendar day**. As a start bound it is local
870
+ * 00:00 of that day; as an end bound it is local 00:00 of the NEXT day, and
871
+ * every consumer compares with `<`. So `to: "2026-08-20"` includes all of
872
+ * the 20th and nothing of the 21st. Resolving it to midnight of the same day
873
+ * is how an end bound quietly excludes the day it names.
874
+ * 2. `YYYY-MM-DD[T ]HH:MM[:SS]` is local wall-clock; with `Z` or `±HH:MM` it is
875
+ * the instant named. A timed bound is used as given, and is exclusive at the
876
+ * end edge like everything else.
877
+ * 3. Relative forms: `±N m|h|d|w`, `today` / `yesterday` / `tomorrow` with an
878
+ * optional `HH:MM`, and `next|last <weekday>`. Days and weeks are calendar
879
+ * arithmetic; hours and minutes are elapsed time. That split is not
880
+ * cosmetic — reversing it drifts every bound by an hour twice a year.
881
+ * 4. Anything else, an empty string, or an instant that does not exist
882
+ * (`2026-02-30`, `T25:00`, `T09:60`) throws `InvalidDateError`. **Nothing
883
+ * here ever returns `NaN`**, because a `NaN` reaches SQLite as `NULL` and a
884
+ * `NULL` bound is an empty result rather than a complaint.
885
+ *
886
+ * The forward-only surfaces inherit the backward forms too. A reminder cannot
887
+ * be due `-7d`, but accepting it costs nothing and refusing it in one surface
888
+ * while accepting it in another is exactly the drift this file replaces.
889
+ */
890
+ const DAY_NAMES = [
891
+ "sunday",
892
+ "monday",
893
+ "tuesday",
894
+ "wednesday",
895
+ "thursday",
896
+ "friday",
897
+ "saturday"
898
+ ];
899
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
900
+ const ISO_DATETIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
901
+ const OFFSET = /^([+-])(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks)$/;
902
+ const DAY_WORD = /^(today|tomorrow|yesterday)(?:\s+(\d{1,2}):(\d{2}))?$/;
903
+ const RELATIVE_DAY = /^(next|last)\s+([a-z]+)(?:\s+(\d{1,2}):(\d{2}))?$/;
904
+ /** `+02:00` / `-05:00` / `Z` for a given instant, in the system zone. */
905
+ const offsetOf = (d) => {
906
+ const mins = -d.getTimezoneOffset();
907
+ if (mins === 0) return "Z";
908
+ const sign = mins < 0 ? "-" : "+";
909
+ const abs = Math.abs(mins);
910
+ return `${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
533
911
  };
534
- /** The host process may not send Apple Events to the app (osascript -1743). */
535
- var TccDeniedError = class extends AppleAutomationError {
536
- name = "TccDeniedError";
537
- constructor(surface) {
538
- super(`Not authorized to control ${surface.appName}. Grant it in System Settings > Privacy & Security > Automation > (the app running this server) > ${surface.appName}, then restart the server. If no entry appears, the first attempt was denied before the prompt could be answered — run \`tccutil reset AppleEvents\` and try again.`);
539
- }
912
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
913
+ /**
914
+ * Local wall-clock time rendered with its offset.
915
+ *
916
+ * Deliberately not `toISOString()`, which converts to UTC and would report a
917
+ * 09:00 bound as `07:00Z` — correct as an instant, but unreadable in a tool
918
+ * result whose purpose is confirming what the caller asked for.
919
+ */
920
+ const toLocalIso = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${offsetOf(d)}`;
921
+ /** Local midnight on the given calendar day. */
922
+ const startOfLocalDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
923
+ /**
924
+ * The last representable instant of the given calendar day, local.
925
+ *
926
+ * Kept for surfaces that genuinely want an inclusive edge. It is NOT what an
927
+ * end bound resolves to — see `parseBound`.
928
+ */
929
+ const endOfLocalDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
930
+ /** Calendar-aware day arithmetic: same wall-clock time, n days later. */
931
+ const addLocalDays = (d, n) => {
932
+ const out = new Date(d.getTime());
933
+ out.setDate(out.getDate() + n);
934
+ return out;
540
935
  };
541
- /** The app is not running and the operation refuses to launch it. */
542
- var AppNotRunningError = class extends AppleAutomationError {
543
- name = "AppNotRunningError";
544
- constructor(surface) {
545
- super(`${surface.appName} is not running. Read tools do not launch it, because launching it steals focus and can start a sync. Open ${surface.appName} and retry.`);
546
- }
936
+ const at = (day, hours, minutes) => new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes, 0, 0);
937
+ const isLeap = (y) => y % 4 === 0 && y % 100 !== 0 || y % 400 === 0;
938
+ const daysInMonth = (y, mo) => [
939
+ 31,
940
+ isLeap(y) ? 29 : 28,
941
+ 31,
942
+ 30,
943
+ 31,
944
+ 30,
945
+ 31,
946
+ 31,
947
+ 30,
948
+ 31,
949
+ 30,
950
+ 31
951
+ ][mo - 1] ?? 0;
952
+ /**
953
+ * Reject an instant that does not exist, BEFORE building a Date from it.
954
+ *
955
+ * Checking afterwards does not work, for two reasons found the hard way. An
956
+ * out-of-range hour rolls the DAY over, so `2026-08-20T25:00` comes back as the
957
+ * 21st at 01:00 and a naive date comparison blames the day the caller got
958
+ * right. And a zoned string does not answer `NaN` at all:
959
+ * `new Date("2026-02-30T09:00Z")` is 2 March, so there is nothing to catch.
960
+ *
961
+ * Arithmetic on the components has neither problem and does not depend on the
962
+ * zone. The order matters — time first, because that is what rolled the date.
963
+ */
964
+ const assertReal = (field, text, y, mo, d, hh = 0, mm = 0, ss = 0) => {
965
+ if (hh > 23 || mm > 59 || ss > 59) throw new InvalidDateError(field, text, `${pad(hh)}:${pad(mm)} is not a time`);
966
+ if (mo < 1 || mo > 12) throw new InvalidDateError(field, text, `there is no month ${pad(mo)}`);
967
+ if (d < 1 || d > daysInMonth(y, mo)) throw new InvalidDateError(field, text, `there is no day ${pad(d)} in month ${pad(mo)}`);
547
968
  };
548
- /** The app was busy and refused the Apple Event (-1712). Retried once before surfacing. */
549
- var AppBusyError = class extends AppleAutomationError {
550
- name = "AppBusyError";
551
- constructor(surface) {
552
- super(`${surface.appName} is busy (probably syncing) and did not answer in time. Retry in a few seconds.`);
969
+ const result = (kind, when, raw) => ({
970
+ kind,
971
+ at: when,
972
+ iso: toLocalIso(when),
973
+ raw
974
+ });
975
+ /**
976
+ * Parse one date argument.
977
+ *
978
+ * @param field Named in the error, so a failure says *which* argument was bad.
979
+ * @param raw The caller's string.
980
+ * @param now Injected for hermetic tests, mirroring `loadConfig(env)`.
981
+ */
982
+ const parseDate = (field, raw, now = /* @__PURE__ */ new Date()) => {
983
+ const text = String(raw ?? "").trim();
984
+ if (!text) throw new InvalidDateError(field, String(raw), "it is empty");
985
+ const lower = text.toLowerCase();
986
+ const dt = ISO_DATETIME.exec(text);
987
+ if (dt) {
988
+ const [, y, mo, d, hh, mm, ss, zone] = dt;
989
+ assertReal(field, text, Number(y), Number(mo), Number(d), Number(hh), Number(mm), Number(ss ?? "0"));
990
+ const when = zone ? new Date(text.replace(" ", "T")) : new Date(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss ?? "0"), 0);
991
+ if (Number.isNaN(when.getTime())) throw new InvalidDateError(field, text, "it is not a real date");
992
+ return result("timed", when, text);
553
993
  }
554
- };
555
- /** osascript exceeded its budget and was killed. */
556
- var OsascriptTimeoutError = class extends AppleAutomationError {
557
- name = "OsascriptTimeoutError";
558
- constructor(timeoutMs, surface) {
559
- super(`${surface.appName} did not answer within ${timeoutMs}ms. It may be mid-sync, or a permission prompt may be waiting on screen. Raise ${surface.envPrefix}_OSASCRIPT_TIMEOUT_MS if this is routine at your data size.`);
994
+ const only = ISO_DATE.exec(text);
995
+ if (only) {
996
+ const [, y, mo, d] = only;
997
+ assertReal(field, text, Number(y), Number(mo), Number(d));
998
+ const when = new Date(Number(y), Number(mo) - 1, Number(d), 0, 0, 0, 0);
999
+ if (Number.isNaN(when.getTime())) throw new InvalidDateError(field, text, "it is not a real date");
1000
+ return result("allDay", when, text);
1001
+ }
1002
+ const off = OFFSET.exec(lower);
1003
+ if (off) {
1004
+ const sign = off[1] === "-" ? -1 : 1;
1005
+ const n = Number(off[2]) * sign;
1006
+ const unit = String(off[3]);
1007
+ if (!Number.isFinite(n)) throw new InvalidDateError(field, text, "the amount is not a number");
1008
+ if (unit.startsWith("d")) return result("timed", addLocalDays(now, n), text);
1009
+ if (unit.startsWith("w")) return result("timed", addLocalDays(now, n * 7), text);
1010
+ const ms = unit.startsWith("h") ? n * 36e5 : n * 6e4;
1011
+ return result("timed", new Date(now.getTime() + ms), text);
560
1012
  }
1013
+ const word = DAY_WORD.exec(lower);
1014
+ if (word) {
1015
+ const shift = word[1] === "tomorrow" ? 1 : word[1] === "yesterday" ? -1 : 0;
1016
+ const day = addLocalDays(now, shift);
1017
+ if (word[2] === void 0) return result("allDay", startOfLocalDay(day), text);
1018
+ const [hh, mm] = [Number(word[2]), Number(word[3])];
1019
+ if (hh > 23 || mm > 59) throw new InvalidDateError(field, text, `${hh}:${word[3]} is not a time`);
1020
+ return result("timed", at(day, hh, mm), text);
1021
+ }
1022
+ const rel = RELATIVE_DAY.exec(lower);
1023
+ if (rel) {
1024
+ const idx = DAY_NAMES.findIndex((n) => n === rel[2] || n.slice(0, 3) === rel[2]);
1025
+ if (idx === -1) throw new InvalidDateError(field, text, `"${rel[2]}" is not a day of the week`);
1026
+ const delta = rel[1] === "next" ? (idx - now.getDay() + 7) % 7 || 7 : -((now.getDay() - idx + 7) % 7 || 7);
1027
+ const day = addLocalDays(now, delta);
1028
+ if (rel[3] === void 0) return result("allDay", startOfLocalDay(day), text);
1029
+ const [hh, mm] = [Number(rel[3]), Number(rel[4])];
1030
+ if (hh > 23 || mm > 59) throw new InvalidDateError(field, text, `${hh}:${rel[4]} is not a time`);
1031
+ return result("timed", at(day, hh, mm), text);
1032
+ }
1033
+ throw new InvalidDateError(field, text, "it matches none of the accepted forms");
561
1034
  };
562
1035
  /**
563
- * A write was attempted while writes are disabled. Under the house pattern write
564
- * tools are not registered at all when `allowWrites` is off, so this is a
565
- * belt-and-braces guard for the library surface, not a path tools can reach.
1036
+ * Parse a bound for a range filter.
1037
+ *
1038
+ * A bare day names the WHOLE day, so which instant it resolves to depends on
1039
+ * which edge it is. The start edge is that day's local midnight. The end edge
1040
+ * is the NEXT day's local midnight, and every caller compares with `<` — which
1041
+ * makes the bound exclusive and the named day fully included.
1042
+ *
1043
+ * The alternative, 23:59:59.999, was what two of the three copies did, and it
1044
+ * is wrong twice: it drops the last millisecond of the day, and more to the
1045
+ * point it invites the `<=` that then drops the whole day when somebody writes
1046
+ * midnight instead.
566
1047
  */
567
- var WritesDisabledError = class extends AppleAutomationError {
568
- name = "WritesDisabledError";
569
- constructor(surface) {
570
- super(`Writes are disabled. Set ${surface.envPrefix}_ALLOW_WRITES=1 to enable the mutating tools.`);
571
- }
572
- };
573
- /** A read-only index could not be opened, so the file lane is unavailable. */
574
- var IndexUnavailableError = class extends AppleAutomationError {
575
- name = "IndexUnavailableError";
576
- };
577
- /** The store's schema is not the one we know how to read. */
578
- var SchemaDriftError = class extends AppleAutomationError {
579
- name = "SchemaDriftError";
580
- };
581
- /** The server is not running on macOS, or osascript is missing. */
582
- var PlatformError = class extends AppleAutomationError {
583
- name = "PlatformError";
584
- };
585
- /** osascript exited 0 but did not produce the JSON envelope we require. */
586
- var ProtocolError = class extends AppleAutomationError {
587
- name = "ProtocolError";
588
- };
589
- /** A local precondition failed before anything was sent to the app. */
590
- var PreconditionError = class extends AppleAutomationError {
591
- name = "PreconditionError";
1048
+ const parseBound = (field, raw, edge, now = /* @__PURE__ */ new Date()) => {
1049
+ const parsed = parseDate(field, raw, now);
1050
+ if (parsed.kind !== "allDay") return parsed.at;
1051
+ return edge === "end" ? startOfLocalDay(addLocalDays(parsed.at, 1)) : startOfLocalDay(parsed.at);
592
1052
  };
593
1053
  //#endregion
594
1054
  //#region src/tools.ts
@@ -1476,28 +1936,34 @@ const openReadOnly = (path, mode, opts = {}) => {
1476
1936
  if (mode === "off") throw new IndexUnavailableError(`The index lane is disabled${opts.envVar ? ` (${opts.envVar}=off)` : ""}.`);
1477
1937
  const attempts = mode === "auto" ? ["ro", "immutable"] : [mode === "ro" ? "ro" : "immutable"];
1478
1938
  let lastError = null;
1479
- for (const attempt of attempts) try {
1480
- const uri = toFileUri(path, attempt === "ro" ? "mode=ro" : "immutable=1");
1481
- const db = new DatabaseSync(uri, {
1482
- readOnly: true,
1483
- allowExtension: false
1484
- });
1485
- db.exec("PRAGMA query_only = 1");
1486
- const validated = opts.validate?.(db);
1487
- if (attempt === "immutable") opts.onFallback?.();
1488
- return {
1489
- db,
1490
- mode: attempt,
1491
- validated
1492
- };
1493
- } catch (err) {
1494
- if (opts.fatal?.(err)) throw err;
1495
- lastError = err;
1939
+ for (const attempt of attempts) {
1940
+ let db;
1941
+ try {
1942
+ const uri = toFileUri(path, attempt === "ro" ? "mode=ro" : "immutable=1");
1943
+ db = new DatabaseSync(uri, {
1944
+ readOnly: true,
1945
+ allowExtension: false
1946
+ });
1947
+ db.exec("PRAGMA query_only = 1");
1948
+ const validated = opts.validate?.(db);
1949
+ if (attempt === "immutable") opts.onFallback?.();
1950
+ return {
1951
+ db,
1952
+ mode: attempt,
1953
+ validated
1954
+ };
1955
+ } catch (err) {
1956
+ try {
1957
+ db?.close();
1958
+ } catch {}
1959
+ if (opts.fatal?.(err)) throw err;
1960
+ lastError = err;
1961
+ }
1496
1962
  }
1497
1963
  const message = lastError instanceof Error ? lastError.message : String(lastError);
1498
1964
  throw new IndexUnavailableError(`Could not open ${opts.label ?? path} at ${path}: ${message}.${opts.hint ? ` ${opts.hint}` : ""}`);
1499
1965
  };
1500
1966
  //#endregion
1501
- export { AppBusyError, AppNotRunningError, AppleAutomationError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, OsascriptTimeoutError, PlatformError, PreconditionError, ProtocolError, RESOURCE_SCHEME, SchemaDriftError, TccDeniedError, WritesDisabledError, assertStaticScript, columnsOf, compact, confirmArg, createOsascriptRunner, describeAggregation, describeStore, detectEpoch, escapeLike, extractCode, fail, fingerprintSchema, groupByArg, inspectFile, limitArg, mapOsaError, ok, okText, openReadOnly, parseBool, parseConfig, parseIntOpt, parseList, project, promptArg, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, requiredPromptArg, resolveLimit, runStdioServer, selectArg, surfaceUri, tableMap, toFailure, toFileUri, trimToolListing, trimmed, withBusyRetry, withLazyTools, withTrimmedListing, wrap, wrapResult };
1967
+ export { AppBusyError, AppNotRunningError, AppleAutomationError, AxChannelError, BaseConfigSchema, CORE_DATA_EPOCH_OFFSET, IndexUnavailableError, InvalidDateError, OsascriptTimeoutError, PlatformError, PreconditionError, ProtocolError, RESOURCE_SCHEME, SchemaDriftError, TccDeniedError, WritesDisabledError, addLocalDays, assertStaticScript, columnsOf, compact, confirmArg, createOsascriptRunner, describeAggregation, describeStore, detectEpoch, endOfLocalDay, escapeLike, extractCode, fail, fingerprintSchema, groupByArg, inspectFile, interferenceNote, limitArg, mapOsaError, ok, okText, openAxChannel, openReadOnly, parseBool, parseBound, parseConfig, parseDate, parseIntOpt, parseList, project, promptArg, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, requiredPromptArg, resolveLimit, runStdioServer, selectArg, startOfLocalDay, surfaceUri, tableMap, toFailure, toFileUri, toLocalIso, trimToolListing, trimmed, watchInterference, withBusyRetry, withLazyTools, withTrimmedListing, wrap, wrapResult };
1502
1968
 
1503
1969
  //# sourceMappingURL=index.js.map