@mgcrea/mcp-apple-core 1.14.0 → 1.16.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,309 @@
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
+ //#endregion
78
+ //#region src/ax.ts
79
+ /**
80
+ * Borrowing the app's Accessibility driver.
81
+ *
82
+ * ## Why this exists at all
83
+ *
84
+ * `AXUIElementCopyAttributeValue` reads an attribute in **0.202 ms**. Reaching
85
+ * the same attribute through `osascript` + JXA + System Events costs **47.4 ms**
86
+ * — a 234x difference that is entirely transport, measured both ways on one
87
+ * machine in `docs/desktop.md`. Every Accessibility number this repo recorded
88
+ * before that document was taken through System Events, which is why the lane
89
+ * was closed three times on figures that were pricing the wrong thing.
90
+ *
91
+ * The native call cannot be made from here. Node has no binding for it, and
92
+ * more importantly the grant does not belong to node: TCC attaches
93
+ * Accessibility to the **responsible GUI ancestor**, so it is Cupertino.app
94
+ * that holds it. Hence a channel rather than a port — the app already runs the
95
+ * driver, and `ServerHost` will lend it for one bundle id.
96
+ *
97
+ * ## The second grant this removes, which matters more than the speed
98
+ *
99
+ * Driving a UI through System Events needs Automation-to-System-Events **on top
100
+ * of** Accessibility. Two grants, given in two different System Settings panes,
101
+ * to reach one window. The native driver needs the first and not the second.
102
+ *
103
+ * ## Absence is the fallback, not an error
104
+ *
105
+ * `CUPERTINO_AX_SOCKET` and `CUPERTINO_AX_FOR` are set by `ServerLocator` and by
106
+ * nothing else. A package installed from npm and run by hand has neither, and
107
+ * that is the supported case: these are published artifacts that must work with
108
+ * no app on the machine. `open()` returns null there, and the caller keeps
109
+ * whatever lane it had. It never throws to say "no app".
110
+ *
111
+ * ## Lazy, deliberately
112
+ *
113
+ * Nothing connects at construction. `SurfaceCatalog` probes servers to learn
114
+ * what they register, and a probe that opened a socket would make every capability
115
+ * scan depend on the host being ready to answer one.
116
+ */
117
+ /** The handshake `ServerHost` expects, and the reply it sends back. */
118
+ const PROTOCOL = "cupertino/1";
119
+ const LENT_SURFACE = "desktop";
120
+ const OK = "ok";
121
+ /**
122
+ * How long one call may take.
123
+ *
124
+ * A walk carries its own five-second budget and the AX messaging timeout is two
125
+ * seconds per element, so the ceiling that matters is the app's, not this one's.
126
+ * This is only here so a host that stopped answering fails rather than hanging a
127
+ * tool call forever.
128
+ */
129
+ const CALL_TIMEOUT_MS = 3e4;
130
+ /**
131
+ * The app is there and said no, or stopped answering.
132
+ *
133
+ * Distinct from the null `openAxChannel` returns, which means there is no app
134
+ * to ask — see the note there on why those two must not collapse.
135
+ */
136
+ var AxChannelError = class extends AppleAutomationError {
137
+ name = "AxChannelError";
138
+ constructor(surface, message) {
139
+ super(`${surface.appName}: ${message}`, { surface: surface.appName });
140
+ }
141
+ };
142
+ /** What the host told us, when it refused the handshake. */
143
+ const refusal = (line) => line.startsWith("err ") ? line.slice(4) : `unexpected handshake reply '${line}'`;
144
+ /**
145
+ * Open the channel, or return null when this server is not hosted by the app.
146
+ *
147
+ * Null and a throw are different answers and the difference is the whole
148
+ * contract: null means "there is no app here, use your other lane", a throw
149
+ * means "there is an app and it said no", which a caller must report rather
150
+ * than paper over.
151
+ */
152
+ const openAxChannel = (surface, env = process.env) => {
153
+ const socketPath = env.CUPERTINO_AX_SOCKET?.trim();
154
+ const identity = env.CUPERTINO_AX_FOR?.trim();
155
+ if (!socketPath || !identity) return null;
156
+ let socket = null;
157
+ let pending = Promise.resolve();
158
+ let nextId = 1;
159
+ const connectOnce = () => new Promise((resolve, reject) => {
160
+ const s = connect(socketPath);
161
+ let buffer = "";
162
+ const onError = (error) => {
163
+ s.destroy();
164
+ reject(new AxChannelError(surface, `cannot reach Cupertino: ${error.message}`));
165
+ };
166
+ s.once("error", onError);
167
+ s.setEncoding("utf8");
168
+ const onData = (chunk) => {
169
+ buffer += chunk;
170
+ const newline = buffer.indexOf("\n");
171
+ if (newline < 0) return;
172
+ const line = buffer.slice(0, newline).trim();
173
+ s.off("data", onData);
174
+ s.off("error", onError);
175
+ if (line !== OK) {
176
+ s.destroy();
177
+ reject(new AxChannelError(surface, refusal(line)));
178
+ return;
179
+ }
180
+ resolve(s);
181
+ };
182
+ s.on("data", onData);
183
+ s.write(`${PROTOCOL} ${LENT_SURFACE} for=${identity}\n`);
184
+ });
185
+ const request = async (payload) => {
186
+ socket ??= await connectOnce();
187
+ const live = socket;
188
+ const id = nextId++;
189
+ const line = `${JSON.stringify({
190
+ jsonrpc: "2.0",
191
+ id,
192
+ method: "tools/call",
193
+ params: {
194
+ name: payload.tool,
195
+ arguments: payload.args
196
+ }
197
+ })}\n`;
198
+ return new Promise((resolve, reject) => {
199
+ let buffer = "";
200
+ const timer = setTimeout(() => {
201
+ cleanup();
202
+ reject(new AxChannelError(surface, `${payload.tool} did not answer in ${CALL_TIMEOUT_MS}ms`));
203
+ }, CALL_TIMEOUT_MS);
204
+ const cleanup = () => {
205
+ clearTimeout(timer);
206
+ live.off("data", onData);
207
+ live.off("error", onError);
208
+ live.off("close", onClose);
209
+ };
210
+ const onError = (error) => {
211
+ cleanup();
212
+ reject(new AxChannelError(surface, `${payload.tool} failed: ${error.message}`));
213
+ };
214
+ const onClose = () => {
215
+ cleanup();
216
+ socket = null;
217
+ reject(new AxChannelError(surface, `Cupertino closed the connection during ${payload.tool}`));
218
+ };
219
+ const onData = (chunk) => {
220
+ buffer += chunk;
221
+ const newline = buffer.indexOf("\n");
222
+ if (newline < 0) return;
223
+ const raw = buffer.slice(0, newline);
224
+ cleanup();
225
+ let message;
226
+ try {
227
+ message = JSON.parse(raw);
228
+ } catch {
229
+ reject(new AxChannelError(surface, `unparseable reply to ${payload.tool}`));
230
+ return;
231
+ }
232
+ if (message.error) {
233
+ reject(new AxChannelError(surface, message.error.message ?? "unknown error"));
234
+ return;
235
+ }
236
+ const text = message.result?.content?.[0]?.text;
237
+ if (text === void 0) {
238
+ reject(new AxChannelError(surface, `empty reply to ${payload.tool}`));
239
+ return;
240
+ }
241
+ if (message.result?.isError === true) {
242
+ reject(new AxChannelError(surface, `${payload.tool}: ${text}`));
243
+ return;
244
+ }
245
+ try {
246
+ resolve(JSON.parse(text));
247
+ } catch {
248
+ resolve(text);
249
+ }
250
+ };
251
+ live.on("data", onData);
252
+ live.once("error", onError);
253
+ live.once("close", onClose);
254
+ live.write(line);
255
+ });
256
+ };
257
+ return {
258
+ call(payload) {
259
+ const result = pending.then(() => request(payload));
260
+ pending = result.then(() => void 0, () => void 0);
261
+ return result;
262
+ },
263
+ close() {
264
+ socket?.destroy();
265
+ socket = null;
266
+ }
267
+ };
268
+ };
269
+ /**
270
+ * Start watching. `check()` answers for the span since this call.
271
+ *
272
+ * Returns null from `check()` when the question could not be asked at all,
273
+ * which is not the same as "undisturbed" and must not be reported as it.
274
+ */
275
+ const watchInterference = (channel) => {
276
+ const started = Date.now();
277
+ return { async check() {
278
+ const elapsedSeconds = (Date.now() - started) / 1e3;
279
+ try {
280
+ const secondsSinceInput = (await channel.call({
281
+ tool: "apple_desktop_user_activity",
282
+ args: {}
283
+ })).secondsSinceInput;
284
+ if (typeof secondsSinceInput !== "number") return null;
285
+ return {
286
+ disturbed: secondsSinceInput < elapsedSeconds,
287
+ secondsSinceInput,
288
+ elapsedSeconds
289
+ };
290
+ } catch {
291
+ return null;
292
+ }
293
+ } };
294
+ };
295
+ /**
296
+ * The sentence to append to a failure, or "" when nothing useful can be said.
297
+ *
298
+ * Deliberately says nothing when the machine was quiet: a failure that reads
299
+ * "nobody touched it" invites the reader to stop looking, and the point of this
300
+ * is to send them to the RIGHT place rather than to reassure them.
301
+ */
302
+ const interferenceNote = (found) => {
303
+ if (!found?.disturbed) return "";
304
+ 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.`;
305
+ };
306
+ //#endregion
7
307
  //#region src/build-info.ts
8
308
  /**
9
309
  * Read a package's own name and version at startup, so they are always accurate
@@ -22,6 +322,113 @@ const readPackageIdentity = (packageJsonUrl, fallback) => {
22
322
  }
23
323
  };
24
324
  //#endregion
325
+ //#region src/listing.ts
326
+ /**
327
+ * Trimming the SDK's own boilerplate out of `tools/list`.
328
+ *
329
+ * ## What this drops
330
+ *
331
+ * The SDK builds each tool's `inputSchema` from its zod shape at listing time,
332
+ * and the generator stamps every one with
333
+ * `"$schema": "http://json-schema.org/draft-07/schema#"`. Measured across the
334
+ * eight servers with writes on, that one constant is 4,836 B of a 106,157 B
335
+ * listing — 4.6%, paid by every client on every connect, to name a JSON Schema
336
+ * draft the client already has to assume in order to read the rest of the
337
+ * document. Nothing in the protocol reads it and no client needs it, so it is
338
+ * the rare cut that is free rather than a trade.
339
+ *
340
+ * ## What this deliberately does NOT drop
341
+ *
342
+ * `"execution": {"taskSupport": "forbidden"}` is another 3,720 B (3.5%) of
343
+ * identical constant — `registerTool` hardcodes it on every tool — and it looks
344
+ * like the same kind of waste. It is not, and the difference is worth the
345
+ * paragraph so nobody "finishes the job" later.
346
+ *
347
+ * Server-side the two spellings are the same: the SDK's `tools/call` path
348
+ * branches only on `'required'` and `'optional'`, so an absent `execution` and
349
+ * an explicit `'forbidden'` both fall through to the normal handler. Client-side
350
+ * they are not. `taskSupport` is declared `.optional()` with no default, so
351
+ * absence means "unspecified" rather than "forbidden", and a task-capable client
352
+ * reading a listing with no `execution` is entitled to try task augmentation on
353
+ * a tool that was registered without a task handler. `'forbidden'` is the value
354
+ * that tells it not to. Dropping it would trade 930 tokens for a behavioural
355
+ * change on a path nothing here tests.
356
+ *
357
+ * ## Why it is done to the outgoing frame
358
+ *
359
+ * The alternative seams are worse. The schema is generated inside the SDK, so
360
+ * there is no option to pass; overriding the `tools/list` request handler means
361
+ * reaching into `Server._requestHandlers`, a private field, and re-implementing
362
+ * the listing it already builds. Wrapping `Transport.send` is public API, is
363
+ * indifferent to how the listing was produced, and costs nothing on the frames
364
+ * it does not match — every non-listing message is returned by identity below.
365
+ */
366
+ /** The one key removed, spelled once. */
367
+ const GENERATED_SCHEMA_KEY = "$schema";
368
+ /**
369
+ * A schema object without its `$schema` stamp, or the value unchanged.
370
+ *
371
+ * Returns the ORIGINAL reference when there is nothing to do, which is what
372
+ * lets `trimToolListing` decide by identity whether it needs to rebuild
373
+ * anything at all.
374
+ */
375
+ const withoutSchemaKey = (schema) => {
376
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) return schema;
377
+ if (!(GENERATED_SCHEMA_KEY in schema)) return schema;
378
+ const rest = { ...schema };
379
+ delete rest[GENERATED_SCHEMA_KEY];
380
+ return rest;
381
+ };
382
+ /**
383
+ * Strip generated boilerplate from a `tools/list` reply, passing every other
384
+ * message through untouched.
385
+ *
386
+ * Copies rather than mutates. The SDK hands out the registered tool's own
387
+ * schema object, and deleting a key from it would edit the server's state from
388
+ * a function whose job is to shape one reply.
389
+ */
390
+ const trimToolListing = (message) => {
391
+ if (!("result" in message)) return message;
392
+ const result = message.result;
393
+ const tools = result?.tools;
394
+ if (!Array.isArray(tools)) return message;
395
+ let changed = false;
396
+ const trimmed = tools.map((tool) => {
397
+ if (tool === null || typeof tool !== "object") return tool;
398
+ const entry = tool;
399
+ const inputSchema = withoutSchemaKey(entry["inputSchema"]);
400
+ const outputSchema = withoutSchemaKey(entry["outputSchema"]);
401
+ if (inputSchema === entry["inputSchema"] && outputSchema === entry["outputSchema"]) return tool;
402
+ changed = true;
403
+ return {
404
+ ...entry,
405
+ ...entry["inputSchema"] === void 0 ? {} : { inputSchema },
406
+ ...entry["outputSchema"] === void 0 ? {} : { outputSchema }
407
+ };
408
+ });
409
+ if (!changed) return message;
410
+ return {
411
+ ...message,
412
+ result: {
413
+ ...result,
414
+ tools: trimmed
415
+ }
416
+ };
417
+ };
418
+ /**
419
+ * Wrap a transport so every listing it sends is trimmed on the way out.
420
+ *
421
+ * Mutates and returns the transport it was given rather than proxying it: the
422
+ * SDK's `connect` reaches for `onmessage`, `onclose` and `onerror` on the very
423
+ * object it was handed, and a Proxy or a subclass would have to keep those in
424
+ * sync for no gain.
425
+ */
426
+ const withTrimmedListing = (transport) => {
427
+ const send = transport.send.bind(transport);
428
+ transport.send = (message, options) => send(trimToolListing(message), options);
429
+ return transport;
430
+ };
431
+ //#endregion
25
432
  //#region src/cli.ts
26
433
  /**
27
434
  * Boot a server on stdio.
@@ -47,7 +454,7 @@ const runStdioServer = async (opts) => {
47
454
  process.exit(1);
48
455
  }
49
456
  const { server, banner } = await opts.start(logger);
50
- await server.connect(new StdioServerTransport());
457
+ await server.connect(withTrimmedListing(new StdioServerTransport()));
51
458
  logger.warn(`${logPrefix} connected (${banner})`);
52
459
  const shutdown = (signal) => {
53
460
  logger.warn(`received ${signal}, shutting down`);
@@ -373,6 +780,26 @@ const BaseConfigSchema = z.object({
373
780
  * nothing serves would be a dangling reference by configuration.
374
781
  */
375
782
  exposePrompts: z.boolean().default(true),
783
+ /**
784
+ * Serve a searchable index and a dispatcher instead of the full tool list.
785
+ *
786
+ * OFF by default, and a COST knob like `exposePrompts` above rather than a
787
+ * safety gate — but unlike that one it is a knob that TRADES. See
788
+ * `facade.ts` for the mechanism; the trade is that a host's permission rule
789
+ * stops naming the individual tool and starts naming a direction: one rule
790
+ * for this surface's reads, one for its writes.
791
+ *
792
+ * What it buys, measured with writes on: ~26.5k tokens of tool definitions
793
+ * across the eight servers becomes a handful per surface. What it costs
794
+ * besides the permission granularity is a round trip — a model must search
795
+ * before it can call.
796
+ *
797
+ * Worth switching on only for a client that does not already defer tool
798
+ * schemas itself. Claude Code and Claude Desktop do, and gain nothing here
799
+ * while paying both costs, which is why the app declines to write the flag
800
+ * into their config files at all.
801
+ */
802
+ lazyTools: z.boolean().default(false),
376
803
  debug: z.boolean().default(false),
377
804
  osascriptPath: z.string().default("/usr/bin/osascript"),
378
805
  osascriptTimeoutMs: z.number().int().min(1e3).max(6e5).default(3e4),
@@ -394,74 +821,409 @@ const parseConfig = (schema, raw) => {
394
821
  return parsed.data;
395
822
  };
396
823
  //#endregion
397
- //#region src/errors.ts
398
- /** Base class so `toFailure` can carry structured detail through in one branch. */
399
- var AppleAutomationError = class extends Error {
400
- name = "AppleAutomationError";
401
- details;
402
- constructor(message, details) {
403
- super(message);
404
- this.details = details;
824
+ //#region src/tools.ts
825
+ /**
826
+ * Compact, not pretty-printed.
827
+ *
828
+ * A model does not need the indentation, and it is not free: measured against
829
+ * rows matching these servers' own types, `null, 2` adds 25-41% depending on how
830
+ * many short keys a row carries - worst on the widest lists, which are exactly
831
+ * the responses already big enough to matter. Every tool in every surface
832
+ * returns through here, so this is the one place it is paid.
833
+ */
834
+ const ok = (data) => ({ content: [{
835
+ type: "text",
836
+ text: JSON.stringify(data ?? { ok: true })
837
+ }] });
838
+ /**
839
+ * Return text as-is. `ok()` JSON-stringifies, which turns a message body into
840
+ * one escaped "Hi,\n\n…" line that no one can read.
841
+ */
842
+ const okText = (text) => ({ content: [{
843
+ type: "text",
844
+ text
845
+ }] });
846
+ const fail = (message, extra) => ({
847
+ content: [{
848
+ type: "text",
849
+ text: JSON.stringify({
850
+ error: message,
851
+ ...extra ? { details: extra } : {}
852
+ })
853
+ }],
854
+ isError: true
855
+ });
856
+ /** Render a thrown value as a tool error, preserving whatever detail it carried. */
857
+ const toFailure = (err) => {
858
+ if (err instanceof AppleAutomationError) return fail(err.message, {
859
+ kind: err.name,
860
+ ...err.details
861
+ });
862
+ if (err instanceof Error) {
863
+ const details = err.details;
864
+ return fail(err.message, details);
405
865
  }
866
+ return fail("Unknown error", err);
406
867
  };
407
- /** The host process may not send Apple Events to the app (osascript -1743). */
408
- var TccDeniedError = class extends AppleAutomationError {
409
- name = "TccDeniedError";
410
- constructor(surface) {
411
- 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.`);
868
+ /** Run a tool body, JSON-formatting the result and turning errors into a tool error. */
869
+ const wrap = async (fn) => {
870
+ try {
871
+ return ok(await fn());
872
+ } catch (err) {
873
+ return toFailure(err);
412
874
  }
413
875
  };
414
- /** The app is not running and the operation refuses to launch it. */
415
- var AppNotRunningError = class extends AppleAutomationError {
416
- name = "AppNotRunningError";
417
- constructor(surface) {
418
- 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.`);
876
+ /** Like `wrap`, but the body chooses its own result shape (e.g. a raw body). */
877
+ const wrapResult = async (fn) => {
878
+ try {
879
+ return await fn();
880
+ } catch (err) {
881
+ return toFailure(err);
419
882
  }
420
883
  };
421
- /** The app was busy and refused the Apple Event (-1712). Retried once before surfacing. */
422
- var AppBusyError = class extends AppleAutomationError {
423
- name = "AppBusyError";
424
- constructor(surface) {
425
- super(`${surface.appName} is busy (probably syncing) and did not answer in time. Retry in a few seconds.`);
426
- }
884
+ /** Drop undefined values so we never send `{mailbox: undefined}` down a lane. */
885
+ const compact = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
886
+ const limitArg = z.number().int().min(1).max(200).optional().describe("Maximum number of results. Each tool states its own default; `maxResults` is the ceiling either way.");
887
+ /**
888
+ * Settle a caller's `limit` against the tool's default and the config ceiling.
889
+ *
890
+ * Written out by hand at twenty-odd call sites before this existed, in five
891
+ * different spellings - and three surfaces spelled it `limit ?? maxResults`,
892
+ * with no `Math.min` at all. That made their real default 200 while `limitArg`
893
+ * told every model it was 25, so a model that trusted the description and
894
+ * omitted the argument got eight times the rows it asked for.
895
+ *
896
+ * `fallback` is the tool's own documented default, not a global one: a mailbox
897
+ * listing and a day of events do not want the same number.
898
+ */
899
+ const resolveLimit = (limit, maxResults, fallback = 25) => Math.min(limit ?? fallback, maxResults);
900
+ const confirmArg = z.literal(true).describe("Must be true. This action changes data and is not undoable from here.");
901
+ //#endregion
902
+ //#region src/facade.ts
903
+ /**
904
+ * A stand-in server that records what would have been registered.
905
+ *
906
+ * Typed as a whole `McpServer` and cast once, here, rather than narrowed to a
907
+ * `Pick<…, "registerTool">` that every surface's `registerTools` would then
908
+ * have to widen its parameter to accept — eight signatures and the ~95
909
+ * functions beneath them, changed to describe a fact that is already true.
910
+ *
911
+ * The cast is safe by inspection, and the inspection is the point: all 95
912
+ * `registerTool` call sites across the eight surfaces call this one method and
913
+ * nothing else, and not one uses its return value. If a registrar ever reaches
914
+ * for `registerPrompt` or `server.server`, it will fail here at runtime rather
915
+ * than quietly registering into a void — which is why this returns a bare
916
+ * object instead of a Proxy that would forward the difference to a real server
917
+ * and half-register a surface.
918
+ */
919
+ const recorder = (into) => ({ registerTool: (name, config, handler) => {
920
+ into.push({
921
+ name,
922
+ config,
923
+ handler
924
+ });
925
+ } });
926
+ /** Terms shorter than this are dropped: they match everything and rank nothing. */
927
+ const SHORTEST_TERM = 3;
928
+ /** How much of a description is printed per row. The whole of it is searched. */
929
+ const SUMMARY_LIMIT = 160;
930
+ const SEARCH_LIMIT = 25;
931
+ /** Partial matches are low precision by construction, so fewer of them. */
932
+ const PARTIAL_LIMIT = 10;
933
+ const INDEX_LIMIT = 200;
934
+ /**
935
+ * Rank tiers, best first, SUMMED across the query's terms.
936
+ *
937
+ * Summing rather than taking the best single term is deliberate: a two-word
938
+ * query scored on its luckiest word ranks a tool that matched one term above a
939
+ * tool that matched both.
940
+ */
941
+ const RANK = {
942
+ exactName: 0,
943
+ namePrefix: 1,
944
+ nameSubstring: 2,
945
+ summary: 3,
946
+ tail: 4,
947
+ missing: 5
427
948
  };
428
- /** osascript exceeded its budget and was killed. */
429
- var OsascriptTimeoutError = class extends AppleAutomationError {
430
- name = "OsascriptTimeoutError";
431
- constructor(timeoutMs, surface) {
432
- 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.`);
433
- }
949
+ const summarize = (description) => {
950
+ const flat = description.replace(/\s+/g, " ").trim();
951
+ return flat.length <= SUMMARY_LIMIT ? flat : `${flat.slice(0, 159).trimEnd()}…`;
952
+ };
953
+ const index = (decl) => {
954
+ const description = decl.config.description ?? "";
955
+ const summary = summarize(description);
956
+ return {
957
+ name: decl.name,
958
+ summary,
959
+ head: `${decl.name} ${summary}`.toLowerCase(),
960
+ whole: `${decl.name} ${description}`.toLowerCase()
961
+ };
434
962
  };
435
963
  /**
436
- * A write was attempted while writes are disabled. Under the house pattern write
437
- * tools are not registered at all when `allowWrites` is off, so this is a
438
- * belt-and-braces guard for the library surface, not a path tools can reach.
964
+ * Regular plural fold, guarded.
965
+ *
966
+ * The guards are the whole point: without them `status` searches for `statu`,
967
+ * `class` for `clas` and `focus` for `focu`, and a three-letter term like `ios`
968
+ * loses a third of itself.
439
969
  */
440
- var WritesDisabledError = class extends AppleAutomationError {
441
- name = "WritesDisabledError";
442
- constructor(surface) {
443
- super(`Writes are disabled. Set ${surface.envPrefix}_ALLOW_WRITES=1 to enable the mutating tools.`);
970
+ const singular = (term) => {
971
+ if (term.length <= SHORTEST_TERM) return term;
972
+ if (term.endsWith("ss") || term.endsWith("us")) return term;
973
+ return term.endsWith("s") ? term.slice(0, -1) : term;
974
+ };
975
+ const queryTerms = (query) => {
976
+ const seen = /* @__PURE__ */ new Set();
977
+ const out = [];
978
+ for (const raw of query.toLowerCase().split(/\s+/)) {
979
+ const term = raw.trim();
980
+ if (term.length < SHORTEST_TERM || seen.has(term)) continue;
981
+ seen.add(term);
982
+ out.push(term);
444
983
  }
984
+ return out;
445
985
  };
446
- /** A read-only index could not be opened, so the file lane is unavailable. */
447
- var IndexUnavailableError = class extends AppleAutomationError {
448
- name = "IndexUnavailableError";
986
+ const rankTerm = (term, entry) => {
987
+ const name = entry.name.toLowerCase();
988
+ const folded = singular(term);
989
+ const hit = (haystack) => haystack.includes(term) || haystack.includes(folded);
990
+ if (name === term) return RANK.exactName;
991
+ if (name.startsWith(term)) return RANK.namePrefix;
992
+ if (hit(name)) return RANK.nameSubstring;
993
+ if (hit(entry.head)) return RANK.summary;
994
+ if (hit(entry.whole)) return RANK.tail;
995
+ return RANK.missing;
449
996
  };
450
- /** The store's schema is not the one we know how to read. */
451
- var SchemaDriftError = class extends AppleAutomationError {
452
- name = "SchemaDriftError";
997
+ /**
998
+ * Find tools for a query.
999
+ *
1000
+ * An empty query returns the server's OWN order rather than an alphabetised
1001
+ * one: the registrars group related tools together, and sorting throws that
1002
+ * grouping away for no gain.
1003
+ *
1004
+ * Nothing may cache this keyed on the query — a row's tier is a property of the
1005
+ * query AND of the whole catalog it was ranked against.
1006
+ */
1007
+ const find = (entries, query) => {
1008
+ const terms = queryTerms(query);
1009
+ if (terms.length === 0) return {
1010
+ rows: entries.slice(0, INDEX_LIMIT),
1011
+ matched: entries.length,
1012
+ missed: [],
1013
+ partial: false
1014
+ };
1015
+ const scored = entries.map((entry) => {
1016
+ let total = 0;
1017
+ let matched = 0;
1018
+ const missed = [];
1019
+ for (const term of terms) {
1020
+ const rank = rankTerm(term, entry);
1021
+ total += rank;
1022
+ if (rank === RANK.missing) missed.push(term);
1023
+ else matched += 1;
1024
+ }
1025
+ return {
1026
+ entry,
1027
+ total,
1028
+ matched,
1029
+ missed
1030
+ };
1031
+ });
1032
+ const best = scored.reduce((acc, s) => Math.max(acc, s.matched), 0);
1033
+ if (best === 0) return {
1034
+ rows: [],
1035
+ matched: 0,
1036
+ missed: terms,
1037
+ partial: false
1038
+ };
1039
+ const group = scored.filter((s) => s.matched === best).toSorted((a, b) => a.total - b.total || a.entry.name.localeCompare(b.entry.name));
1040
+ const partial = best < terms.length;
1041
+ const missed = partial ? terms.filter((t) => group.every((g) => g.missed.includes(t))) : [];
1042
+ return {
1043
+ rows: group.slice(0, partial ? PARTIAL_LIMIT : SEARCH_LIMIT).map((g) => g.entry),
1044
+ matched: group.length,
1045
+ missed,
1046
+ partial
1047
+ };
453
1048
  };
454
- /** The server is not running on macOS, or osascript is missing. */
455
- var PlatformError = class extends AppleAutomationError {
456
- name = "PlatformError";
1049
+ /**
1050
+ * Suggestions for a name that is not in the catalog.
1051
+ *
1052
+ * Substring search cannot find a string that appears nowhere, so a typo needs
1053
+ * its own answer: shared underscore-separated words first, then the longest
1054
+ * common prefix.
1055
+ *
1056
+ * Both comparisons run on the name with `apple_<surface>_` REMOVED. Every tool
1057
+ * on a surface shares that prefix, so comparing whole names makes every tool
1058
+ * share two words with every other and "did you mean" answers with the first
1059
+ * few tools in the catalog — worse than saying nothing, because it reads like a
1060
+ * real suggestion. Measured before the fix: `apple_mail_send_messge` suggested
1061
+ * `apple_mail_list_accounts`.
1062
+ */
1063
+ const nearest = (names, wanted, prefix) => {
1064
+ const strip = (n) => n.toLowerCase().startsWith(`${prefix}_`) ? n.slice(prefix.length + 1) : n;
1065
+ const target = strip(wanted);
1066
+ const words = new Set(target.toLowerCase().split("_").filter((w) => w.length >= SHORTEST_TERM));
1067
+ const shared = names.filter((n) => strip(n).toLowerCase().split("_").some((w) => words.has(w)));
1068
+ if (shared.length > 0) return shared.slice(0, 5);
1069
+ const common = (n) => {
1070
+ const candidate = strip(n).toLowerCase();
1071
+ const lower = target.toLowerCase();
1072
+ let i = 0;
1073
+ while (i < candidate.length && i < lower.length && candidate[i] === lower[i]) i += 1;
1074
+ return i;
1075
+ };
1076
+ const ranked = names.toSorted((a, b) => common(b) - common(a) || a.localeCompare(b));
1077
+ const bestName = ranked[0];
1078
+ if (bestName === void 0 || common(bestName) < SHORTEST_TERM) return [];
1079
+ return ranked.filter((n) => common(n) >= SHORTEST_TERM).slice(0, 3);
457
1080
  };
458
- /** osascript exited 0 but did not produce the JSON envelope we require. */
459
- var ProtocolError = class extends AppleAutomationError {
460
- name = "ProtocolError";
1081
+ const renderSearch = (result, total, names) => {
1082
+ if (result.rows.length === 0) return `No tool matches. ${total} tools are available — call ${names.search} with no query to list them all.`;
1083
+ const rows = result.rows.map((r) => `${r.name} — ${r.summary}`).join("\n");
1084
+ let notice = "";
1085
+ if (result.partial) notice = `No tool matched every word${result.missed.length > 0 ? ` (no match for ${result.missed.join(", ")})` : ""}. Closest:\n`;
1086
+ const footer = `${result.rows.length} of ${total} tools. Read a schema with ${names.describe}, then run it with ${names.call}.`;
1087
+ return `${notice}${rows}\n\n${footer}`;
461
1088
  };
462
- /** A local precondition failed before anything was sent to the app. */
463
- var PreconditionError = class extends AppleAutomationError {
464
- name = "PreconditionError";
1089
+ /**
1090
+ * A tool's declaration as a client would have received it.
1091
+ *
1092
+ * Built from the recorded zod shape rather than re-derived by hand, so
1093
+ * `describe` and a non-lazy listing cannot drift. `$schema` is dropped for the
1094
+ * same reason `listing.ts` drops it from `tools/list`.
1095
+ */
1096
+ const describe = (decl) => {
1097
+ const shape = decl.config.inputSchema;
1098
+ const schema = shape ? z.toJSONSchema(z.object(shape)) : void 0;
1099
+ if (schema) delete schema["$schema"];
1100
+ return {
1101
+ name: decl.name,
1102
+ ...decl.config.description === void 0 ? {} : { description: decl.config.description },
1103
+ ...schema === void 0 ? {} : { inputSchema: schema },
1104
+ ...decl.config.annotations === void 0 ? {} : { annotations: decl.config.annotations }
1105
+ };
1106
+ };
1107
+ /**
1108
+ * Run a recorded tool, reproducing the validation the SDK would have done.
1109
+ *
1110
+ * Under a facade the SDK never sees the real call, so it never parses the real
1111
+ * arguments. Skipping this would hand every handler unvalidated input — the one
1112
+ * way a facade can be actively less safe than the listing it replaced.
1113
+ */
1114
+ const invoke = async (decl, args, extra) => {
1115
+ const shape = decl.config.inputSchema;
1116
+ if (!shape) return decl.handler(extra);
1117
+ const parsed = await z.object(shape).safeParseAsync(args ?? {});
1118
+ if (!parsed.success) {
1119
+ const why = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
1120
+ return fail(`Invalid arguments for ${decl.name}: ${why}`);
1121
+ }
1122
+ return decl.handler(parsed.data, extra);
1123
+ };
1124
+ /**
1125
+ * Register a surface's tools, either directly or behind the facade.
1126
+ *
1127
+ * The callback takes `allowWrites` rather than closing over it so the facade
1128
+ * can run it a second time with the gate forced shut and learn which tools are
1129
+ * writes. Everything else it needs — the client, the config — it closes over as
1130
+ * before.
1131
+ */
1132
+ const withLazyTools = (server, opts, register) => {
1133
+ if (!opts.lazy) {
1134
+ register(server, opts.allowWrites);
1135
+ return;
1136
+ }
1137
+ const all = [];
1138
+ register(recorder(all), opts.allowWrites);
1139
+ let writeNames = /* @__PURE__ */ new Set();
1140
+ if (opts.allowWrites) {
1141
+ const readsOnly = [];
1142
+ register(recorder(readsOnly), false);
1143
+ const readNames = new Set(readsOnly.map((d) => d.name));
1144
+ writeNames = new Set(all.filter((d) => !readNames.has(d.name)).map((d) => d.name));
1145
+ }
1146
+ const prefix = `apple_${opts.surface}`;
1147
+ const eagerName = `${prefix}_diagnostics`;
1148
+ const eager = all.filter((d) => d.name === eagerName);
1149
+ for (const decl of eager) server.registerTool(decl.name, decl.config, decl.handler);
1150
+ const lazy = all.filter((d) => d.name !== eagerName);
1151
+ const reads = lazy.filter((d) => !writeNames.has(d.name));
1152
+ const writes = lazy.filter((d) => writeNames.has(d.name));
1153
+ const byName = new Map(lazy.map((d) => [d.name, d]));
1154
+ const readIndex = reads.map(index);
1155
+ const writeIndex = writes.map(index);
1156
+ const allIndex = [...readIndex, ...writeIndex];
1157
+ const searchName = `${prefix}_search_tools`;
1158
+ const describeName = `${prefix}_describe_tool`;
1159
+ const callName = `${prefix}_call_tool`;
1160
+ const callWriteName = `${prefix}_call_write_tool`;
1161
+ server.registerTool(searchName, {
1162
+ description: `Find ${opts.displayName} tools by what you want to do. This server loads its ${allIndex.length} tools on demand: they are not listed up front, and this is how you reach them. Returns matching tool names with a one-line summary each. Call with no query to list everything. Read a schema with ${describeName}, then run it with ${callName}` + (writes.length > 0 ? ` or ${callWriteName}` : "") + `.`,
1163
+ inputSchema: { query: z.string().optional().describe("What you want to do, e.g. 'search messages' or 'unread'. Omit to list all.") },
1164
+ annotations: {
1165
+ readOnlyHint: true,
1166
+ idempotentHint: true
1167
+ }
1168
+ }, ({ query }) => okText(renderSearch(find(allIndex, query ?? ""), allIndex.length, {
1169
+ search: searchName,
1170
+ describe: describeName,
1171
+ call: callName
1172
+ })));
1173
+ server.registerTool(describeName, {
1174
+ description: `Get the full description and input schema of one ${opts.displayName} tool, as it would have appeared in a normal tool listing. Find names with ${searchName} first.`,
1175
+ inputSchema: { name: z.string().describe(`Exact tool name, e.g. ${lazy[0]?.name ?? callName}.`) },
1176
+ annotations: {
1177
+ readOnlyHint: true,
1178
+ idempotentHint: true
1179
+ }
1180
+ }, ({ name }) => {
1181
+ const decl = byName.get(name);
1182
+ if (!decl) {
1183
+ const suggestions = nearest([...byName.keys()], name, prefix);
1184
+ return fail(`No tool named ${name}.` + (suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""));
1185
+ }
1186
+ const runner = writeNames.has(name) ? callWriteName : callName;
1187
+ return okText(`${JSON.stringify(describe(decl))}\n\nCall it with ${runner}: {"name": ${JSON.stringify(name)}, "arguments": {…}}.`);
1188
+ });
1189
+ server.registerTool(callName, {
1190
+ description: `Run one of this server's read-only ${opts.displayName} tools. Find a name with ${searchName} and its arguments with ${describeName}. Reads only: it cannot reach ` + (writes.length > 0 ? `anything that changes ${opts.displayName} — those go through ${callWriteName}.` : `anything that changes ${opts.displayName}, and this server has writes turned off.`),
1191
+ inputSchema: {
1192
+ name: z.string().describe("Exact tool name to run."),
1193
+ arguments: z.record(z.string(), z.unknown()).optional().describe("That tool's arguments.")
1194
+ },
1195
+ annotations: { readOnlyHint: true }
1196
+ }, async ({ name, arguments: args }, extra) => {
1197
+ const decl = byName.get(name);
1198
+ if (!decl) {
1199
+ const suggestions = nearest([...byName.keys()], name, prefix);
1200
+ return fail(`No tool named ${name}.` + (suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""));
1201
+ }
1202
+ if (writeNames.has(name)) return fail(`${name} changes ${opts.displayName}, so it cannot be run through ${callName}. Use ${callWriteName}.`);
1203
+ return await invoke(decl, args, extra);
1204
+ });
1205
+ if (writes.length > 0) {
1206
+ const writeList = writes.map((d) => d.name.slice(prefix.length + 1)).join(", ");
1207
+ server.registerTool(callWriteName, {
1208
+ description: `Run one of this server's ${writes.length} ${opts.displayName} tools that CHANGE data — ${writeList}. Find arguments with ${describeName}. Read-only tools go through ${callName} instead.`,
1209
+ inputSchema: {
1210
+ name: z.string().describe("Exact tool name to run."),
1211
+ arguments: z.record(z.string(), z.unknown()).optional().describe("That tool's arguments.")
1212
+ },
1213
+ annotations: {
1214
+ readOnlyHint: false,
1215
+ destructiveHint: true
1216
+ }
1217
+ }, async ({ name, arguments: args }, extra) => {
1218
+ const decl = byName.get(name);
1219
+ if (!decl) {
1220
+ const suggestions = nearest([...writeNames], name, prefix);
1221
+ return fail(`No tool named ${name}.` + (suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : ""));
1222
+ }
1223
+ if (!writeNames.has(name)) return fail(`${name} is read-only. Use ${callName}.`);
1224
+ return await invoke(decl, args, extra);
1225
+ });
1226
+ }
465
1227
  };
466
1228
  //#endregion
467
1229
  //#region src/fs.ts
@@ -966,84 +1728,6 @@ const openReadOnly = (path, mode, opts = {}) => {
966
1728
  throw new IndexUnavailableError(`Could not open ${opts.label ?? path} at ${path}: ${message}.${opts.hint ? ` ${opts.hint}` : ""}`);
967
1729
  };
968
1730
  //#endregion
969
- //#region src/tools.ts
970
- /**
971
- * Compact, not pretty-printed.
972
- *
973
- * A model does not need the indentation, and it is not free: measured against
974
- * rows matching these servers' own types, `null, 2` adds 25-41% depending on how
975
- * many short keys a row carries - worst on the widest lists, which are exactly
976
- * the responses already big enough to matter. Every tool in every surface
977
- * returns through here, so this is the one place it is paid.
978
- */
979
- const ok = (data) => ({ content: [{
980
- type: "text",
981
- text: JSON.stringify(data ?? { ok: true })
982
- }] });
983
- /**
984
- * Return text as-is. `ok()` JSON-stringifies, which turns a message body into
985
- * one escaped "Hi,\n\n…" line that no one can read.
986
- */
987
- const okText = (text) => ({ content: [{
988
- type: "text",
989
- text
990
- }] });
991
- const fail = (message, extra) => ({
992
- content: [{
993
- type: "text",
994
- text: JSON.stringify({
995
- error: message,
996
- ...extra ? { details: extra } : {}
997
- })
998
- }],
999
- isError: true
1000
- });
1001
- /** Render a thrown value as a tool error, preserving whatever detail it carried. */
1002
- const toFailure = (err) => {
1003
- if (err instanceof AppleAutomationError) return fail(err.message, {
1004
- kind: err.name,
1005
- ...err.details
1006
- });
1007
- if (err instanceof Error) {
1008
- const details = err.details;
1009
- return fail(err.message, details);
1010
- }
1011
- return fail("Unknown error", err);
1012
- };
1013
- /** Run a tool body, JSON-formatting the result and turning errors into a tool error. */
1014
- const wrap = async (fn) => {
1015
- try {
1016
- return ok(await fn());
1017
- } catch (err) {
1018
- return toFailure(err);
1019
- }
1020
- };
1021
- /** Like `wrap`, but the body chooses its own result shape (e.g. a raw body). */
1022
- const wrapResult = async (fn) => {
1023
- try {
1024
- return await fn();
1025
- } catch (err) {
1026
- return toFailure(err);
1027
- }
1028
- };
1029
- /** Drop undefined values so we never send `{mailbox: undefined}` down a lane. */
1030
- const compact = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
1031
- const limitArg = z.number().int().min(1).max(200).optional().describe("Maximum number of results. Each tool states its own default; `maxResults` is the ceiling either way.");
1032
- /**
1033
- * Settle a caller's `limit` against the tool's default and the config ceiling.
1034
- *
1035
- * Written out by hand at twenty-odd call sites before this existed, in five
1036
- * different spellings - and three surfaces spelled it `limit ?? maxResults`,
1037
- * with no `Math.min` at all. That made their real default 200 while `limitArg`
1038
- * told every model it was 25, so a model that trusted the description and
1039
- * omitted the argument got eight times the rows it asked for.
1040
- *
1041
- * `fallback` is the tool's own documented default, not a global one: a mailbox
1042
- * listing and a day of events do not want the same number.
1043
- */
1044
- const resolveLimit = (limit, maxResults, fallback = 25) => Math.min(limit ?? fallback, maxResults);
1045
- const confirmArg = z.literal(true).describe("Must be true. This action changes data and is not undoable from here.");
1046
- //#endregion
1047
- 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, trimmed, withBusyRetry, wrap, wrapResult };
1731
+ export { AppBusyError, AppNotRunningError, AppleAutomationError, AxChannelError, 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, interferenceNote, limitArg, mapOsaError, ok, okText, openAxChannel, openReadOnly, parseBool, parseConfig, parseIntOpt, parseList, project, promptArg, readPackageIdentity, registerSurfaceResources, registerWorkflowPrompt, requiredPromptArg, resolveLimit, runStdioServer, selectArg, surfaceUri, tableMap, toFailure, toFileUri, trimToolListing, trimmed, watchInterference, withBusyRetry, withLazyTools, withTrimmedListing, wrap, wrapResult };
1048
1732
 
1049
1733
  //# sourceMappingURL=index.js.map