apple-tools-mcp 2.0.0 → 2.0.2

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.
@@ -24,6 +24,7 @@ export const DEFAULT_SCRIPT_TIMEOUT_MS = 30000;
24
24
  export const NOT_FOUND_SENTINELS = [
25
25
  "MESSAGE_NOT_FOUND",
26
26
  "EVENT_NOT_FOUND",
27
+ "EVENTKIT_NOT_FOUND",
27
28
  "CONTACT_NOT_FOUND",
28
29
  "CALENDAR_NOT_FOUND",
29
30
  "CHAT_NOT_FOUND",
@@ -51,29 +52,48 @@ const TCC_SIGNATURES = [
51
52
  ];
52
53
 
53
54
  /**
54
- * These read like "the app is missing", but macOS also emits them when the
55
- * responsible process may not drive the app at all: a denied Automation
56
- * grant frequently surfaces as -1728 / "can't get application" rather than a
57
- * clean -1743. When the app is installed, treat them as attribution
58
- * failures, not as a missing app.
55
+ * Contacts (and sometimes Mail) report these when the app is installed
56
+ * but not launched. That is a cold-start, not an Automation deny.
57
+ * "-1728" / "can't get application" stays on APP_MISSING_SIGNATURES —
58
+ * when the bundle is on disk that is still attribution.
59
59
  */
60
- const APP_MISSING_SIGNATURES = [
60
+ const APP_NOT_RUNNING_SIGNATURES = [
61
61
  "application isn't running",
62
- "can't get application",
63
- "can't get every application",
64
62
  "application is not running",
65
63
  "-600",
64
+ "contacts_not_running"
65
+ ];
66
+
67
+ const APP_MISSING_SIGNATURES = [
68
+ "can't get application",
69
+ "can't get every application",
66
70
  "-1728",
67
71
  "-10810"
68
72
  ];
69
73
 
70
74
  // osascript itself is missing: not macOS, or a stripped PATH. Never an
71
75
  // attribution problem, because nothing ran.
76
+ //
77
+ // Do **not** match a bare "spawnSync osascript" here. A TCC-denied Mail
78
+ // compose hangs until spawnSync times out (`spawnSync osascript ETIMEDOUT`);
79
+ // that string also contains "spawnSync osascript" and must not be reported
80
+ // as "Mail.app could not be reached".
72
81
  const OSASCRIPT_MISSING_SIGNATURES = [
73
- "spawnsync osascript",
82
+ "spawnsync osascript enoent",
74
83
  "enoent"
75
84
  ];
76
85
 
86
+ // Hung Apple events. Mini diagnosis: `tell Mail to get name` returns, but
87
+ // `make new outgoing message` blocks until timeout when node → Mail
88
+ // Automation is denied. That hang is a TCC deny, not a missing app.
89
+ const TIMEOUT_SIGNATURES = [
90
+ "etimedout",
91
+ "timed out after",
92
+ "-1712",
93
+ "appleevent timed out",
94
+ "apple event timed out"
95
+ ];
96
+
77
97
  /**
78
98
  * Standard install locations for the apps this package automates.
79
99
  * Ventura and later keep the first-party apps in /System/Applications.
@@ -98,7 +118,7 @@ export function appBundleInstalled(appName, existsFn = fs.existsSync) {
98
118
  * @param {string} message
99
119
  * @param {object} [context]
100
120
  * @param {boolean|null} [context.appInstalled] - whether the target app exists
101
- * @returns {"tcc"|"not_found"|"attribution"|"app_unavailable"|"unknown"}
121
+ * @returns {"tcc"|"timeout"|"not_found"|"attribution"|"app_not_running"|"app_unavailable"|"unknown"}
102
122
  */
103
123
  export function classifyAppleScriptError(message, context = {}) {
104
124
  const text = String(message || "").toLowerCase();
@@ -106,7 +126,25 @@ export function classifyAppleScriptError(message, context = {}) {
106
126
  if (text.includes(sentinel.toLowerCase())) return "not_found";
107
127
  }
108
128
  if (OSASCRIPT_MISSING_SIGNATURES.some((sig) => text.includes(sig))) return "app_unavailable";
129
+ // ETIMEDOUT / -1712 is a hang, not a TCC deny. Mail maps this kind to
130
+ // MAIL_SEND_TIMEOUT_GUIDANCE (find/reply/open/send), never MAIL_TCC_GUIDANCE.
131
+ // calendar_remove must never print Calendar-denied copy for ETIMEDOUT alone.
132
+ if (TIMEOUT_SIGNATURES.some((sig) => text.includes(sig))) return "timeout";
109
133
  if (TCC_SIGNATURES.some((sig) => text.includes(sig))) return "tcc";
134
+ // Calendar delete of a detached event specifier often returns
135
+ // "Can't get event … (-1728)". That is a missing object, not a missing app.
136
+ // Check before the generic -1728 / "can't get application" path.
137
+ if (
138
+ text.includes("can't get event") ||
139
+ text.includes("can't get calendar") ||
140
+ text.includes("can't get theevent")
141
+ ) {
142
+ return "not_found";
143
+ }
144
+ // Installed + not running (-600) is a cold launch, not Automation.
145
+ if (APP_NOT_RUNNING_SIGNATURES.some((sig) => text.includes(sig))) {
146
+ return context.appInstalled === true ? "app_not_running" : "app_unavailable";
147
+ }
110
148
  if (APP_MISSING_SIGNATURES.some((sig) => text.includes(sig))) {
111
149
  // The app is on disk, so "can't get application" means macOS refused to
112
150
  // let the responsible process drive it - an Automation / attribution
@@ -120,14 +158,111 @@ export function isTccDenial(message) {
120
158
  return classifyAppleScriptError(message) === "tcc";
121
159
  }
122
160
 
161
+ /**
162
+ * Whether dispatchWriteTool should append MacBook/Mini host recovery copy.
163
+ *
164
+ * Handlers rewrite raw Apple events as CONTACTS_/CALENDAR_/MESSAGES_/MAIL
165
+ * TCC guidance or ATTRIBUTION_GUIDANCE, which do not contain the -1743 /
166
+ * -10004 signatures `isTccDenial` looks for (except MAIL_TCC_GUIDANCE).
167
+ * MAIL_SEND_TIMEOUT_GUIDANCE must never match — that hang is not a deny.
168
+ */
169
+ export function needsHostTccAdvice(message) {
170
+ const text = String(message || "");
171
+ if (!text) return false;
172
+ if (text.includes(MAIL_SEND_TIMEOUT_GUIDANCE)) return false;
173
+ if (text.includes(CONTACTS_APP_NOT_RUNNING_GUIDANCE)) return false;
174
+ if (text.includes(MAIL_APP_NOT_RUNNING_GUIDANCE)) return false;
175
+ if (text.includes(MESSAGES_APP_NOT_RUNNING_GUIDANCE)) return false;
176
+ if (text.includes(CALENDAR_APP_NOT_RUNNING_GUIDANCE)) return false;
177
+ if (
178
+ text.includes(CONTACTS_TCC_GUIDANCE) ||
179
+ text.includes(CALENDAR_TCC_GUIDANCE) ||
180
+ text.includes(MESSAGES_TCC_GUIDANCE) ||
181
+ text.includes(MAIL_TCC_GUIDANCE) ||
182
+ text.includes(ATTRIBUTION_GUIDANCE) ||
183
+ text.includes(TCC_GUIDANCE)
184
+ ) {
185
+ return true;
186
+ }
187
+ return isTccDenial(text);
188
+ }
189
+
190
+ /**
191
+ * Real Automation / privacy deny codes — not a hung Calendar.app delete.
192
+ *
193
+ * Mini calendar_remove: add/edit succeed (Automation granted), then delete
194
+ * hits spawnSync ETIMEDOUT / AppleEvent -1712. That is an iCloud/CalDAV
195
+ * hang or a confirmation dialog, not -1743 / -10004. Callers must not
196
+ * rewrite that as CALENDAR_TCC_GUIDANCE.
197
+ */
198
+ const HARD_TCC_SIGNATURES = [
199
+ "-1743",
200
+ "-10004",
201
+ "-25211",
202
+ "not authorized to send apple events",
203
+ "not allowed to send apple events",
204
+ "is not allowed assistive access",
205
+ "operation not permitted",
206
+ "not permitted to access",
207
+ "access to contacts",
208
+ "access to calendars",
209
+ "privacy settings",
210
+ "errae eventnotpermitted",
211
+ "errAEEventNotPermitted".toLowerCase()
212
+ ];
213
+
214
+ export function isHardTccDenial(message) {
215
+ const text = String(message || "").toLowerCase();
216
+ return HARD_TCC_SIGNATURES.some((sig) => text.includes(sig));
217
+ }
218
+
219
+ const APPLEEVENT_CODES = [
220
+ "-1743",
221
+ "-10004",
222
+ "-25211",
223
+ "-1712",
224
+ "-1728",
225
+ "-600",
226
+ "-10810",
227
+ "-1708",
228
+ "-1719",
229
+ "-10025",
230
+ "-2700"
231
+ ];
232
+
233
+ /**
234
+ * Pull known AppleEvent / spawn codes out of osascript stderr so smoke
235
+ * can print them instead of guessing TCC.
236
+ */
237
+ export function extractAppleEventCodes(message) {
238
+ const text = String(message || "");
239
+ const found = [];
240
+ for (const code of APPLEEVENT_CODES) {
241
+ if (text.includes(code) && !found.includes(code)) found.push(code);
242
+ }
243
+ if (/etimedout/i.test(text) && !found.includes("ETIMEDOUT")) found.push("ETIMEDOUT");
244
+ return found;
245
+ }
246
+
247
+ /**
248
+ * One-line osascript diagnostic. Always include this on calendar_remove
249
+ * failure so Mini --apply stops looking like a TCC deny.
250
+ */
251
+ export function formatOsascriptDiagnostic(result, source = "osascript") {
252
+ const raw = String(result && result.error ? result.error : "").replace(/\s+/g, " ").trim();
253
+ const clipped = raw.length > 360 ? `${raw.slice(0, 360)}...` : raw;
254
+ const kind = (result && result.kind) || "unknown";
255
+ const codes = extractAppleEventCodes(raw);
256
+ return `${source} kind=${kind} error=${clipped || "(empty)"}${codes.length ? ` codes=${codes.join(",")}` : ""}`;
257
+ }
258
+
123
259
  /**
124
260
  * Guidance attached to TCC denials. Names the host constraint without
125
261
  * claiming the package can grant another app's entitlements.
126
262
  */
127
263
  export const TCC_GUIDANCE =
128
264
  "macOS denied this automation. TCC attributes Apple events to the process responsible for the MCP server, " +
129
- "so a host app without the matching automation grant blocks the write even when node has Full Disk Access. " +
130
- "Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so writes execute under node, or run this server from a host that can be granted Automation access.";
265
+ "so a host app without the matching automation grant blocks the write even when node has Full Disk Access.";
131
266
 
132
267
  /**
133
268
  * Contacts writes are the sharpest case, and worth separating from contacts
@@ -141,8 +276,28 @@ export const CONTACTS_TCC_GUIDANCE =
141
276
  "macOS denied Contacts access for this write. Contacts writes go through Contacts.app (CNContactStore), which is gated by the AddressBook privacy class " +
142
277
  "and attributed to the process responsible for this MCP server - not to node. A host app that was built without the AddressBook entitlement " +
143
278
  "(com.apple.security.personal-information.addressbook) is denied with no prompt, and that cannot be fixed with Full Disk Access or tccutil. " +
144
- "Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access. " +
145
- "Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so Contacts writes execute under node, which macOS can grant AddressBook access to.";
279
+ "Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access.";
280
+
281
+ /**
282
+ * Contacts.app was installed but not running. Cold `tell application
283
+ * "Contacts"` under launchd often returns -600 instead of auto-launching.
284
+ * That is not -1743 / -10004 and not attribution.
285
+ */
286
+ export const CONTACTS_APP_NOT_RUNNING_GUIDANCE =
287
+ "Contacts.app was not running and did not become ready after launch. This is not a TCC / Automation deny (-1743 / -10004) " +
288
+ "and not an attribution / responsible-process failure. The write path launches Contacts.app; if this persists, open Contacts.app and retry.";
289
+
290
+ export const MAIL_APP_NOT_RUNNING_GUIDANCE =
291
+ "Mail.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
292
+ "This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
293
+
294
+ export const MESSAGES_APP_NOT_RUNNING_GUIDANCE =
295
+ "Messages.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
296
+ "This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
297
+
298
+ export const CALENDAR_APP_NOT_RUNNING_GUIDANCE =
299
+ "Calendar.app was not running. Calendar writes use EventKit and do not require Calendar.app to stay open. " +
300
+ "This is not a TCC / Automation deny (-1743 / -10004).";
146
301
 
147
302
  /**
148
303
  * Calendar has the same split as Contacts: reads in this package are sqlite
@@ -153,8 +308,7 @@ export const CALENDAR_TCC_GUIDANCE =
153
308
  "macOS denied Calendar access for this write. Calendar writes go through Calendar.app (EventKit), which is gated by the calendars privacy class " +
154
309
  "(com.apple.security.personal-information.calendars) and attributed to the process responsible for this MCP server - not to node. " +
155
310
  "A host app built without that entitlement is denied with no prompt, and Full Disk Access or tccutil cannot change it. " +
156
- "Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access. " +
157
- "Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so Calendar writes execute under node, which macOS can grant Calendars access to.";
311
+ "Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access.";
158
312
 
159
313
  /**
160
314
  * Shown when the target app is installed but macOS still refused the Apple
@@ -164,15 +318,49 @@ export const CALENDAR_TCC_GUIDANCE =
164
318
  */
165
319
  export const ATTRIBUTION_GUIDANCE =
166
320
  "The app is installed, so this is an Automation / responsible-process problem rather than a missing app: macOS refused to let the process " +
167
- "responsible for this server drive it. Start the indexer daemon (apple-tools-indexer / the LaunchAgent) so writes run through the write bridge " +
168
- "under launchd-owned node, or run this server from a parent that can hold Automation access (Terminal.app) and approve the prompt.";
321
+ "responsible for this server drive it.";
322
+
323
+ /**
324
+ * Mail writes are gated by Automation → Mail for the responsible process.
325
+ * Shown only for a hard deny (-1743 / -10004 / "not authorized…").
326
+ * ETIMEDOUT / -1712 is `MAIL_SEND_TIMEOUT_GUIDANCE`, including find/reply/open
327
+ * before send. dry_run never sends Apple events to Mail.
328
+ */
329
+ export const MAIL_TCC_GUIDANCE =
330
+ "macOS denied Mail automation (Apple Events to Mail.app). Real denials report -1743, -10004, or \"not authorized to send Apple events\" — a TCC / Automation deny for node → Mail, " +
331
+ "not Mail.app missing or unavailable. dry_run never talks to Mail, so it cannot detect this grant. " +
332
+ "Allow node in System Settings → Privacy & Security → Automation for Mail (same Allow-via-prompt as Contacts and Calendar — do not add node via + in a privacy list). " +
333
+ "A Contacts or Calendar grant does not include Mail.";
334
+
335
+ /**
336
+ * AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
337
+ * or sending. That is not -1743 / -10004, including before send.
338
+ * Clients must Sent-check before retrying a send — a retry of a delivered
339
+ * send creates a second copy. This is not a silent TCC grant.
340
+ */
341
+ export const MAIL_SEND_TIMEOUT_GUIDANCE =
342
+ "Mail AppleScript timed out (ETIMEDOUT / -1712 / AppleEvent timed out) — this is a find/reply/send hang (including find/reply/open before send), not a TCC / Automation deny. " +
343
+ "Real Mail Automation denials report -1743, -10004, or \"not authorized to send Apple events\". " +
344
+ "A timeout can happen after Mail already delivered the message. Check Sent (and Outbox) for this send before retrying; a retry after a successful delivery sends a second copy. " +
345
+ "This tool verifies Sent when a send hangs; if nothing is there yet, wait and look again rather than immediately resending.";
346
+
347
+ /**
348
+ * Messages writes are a separate Automation target from Mail / Contacts / Calendar.
349
+ */
350
+ export const MESSAGES_TCC_GUIDANCE =
351
+ "macOS denied Messages automation (Apple Events to Messages.app). A hang or timeout on send is a TCC / Automation deny for node → Messages, " +
352
+ "not Messages.app missing or unavailable. dry_run never talks to Messages, so it cannot detect this grant. " +
353
+ "Allow node in System Settings → Privacy & Security → Automation for Messages (same Allow-via-prompt as Mail, Contacts, and Calendar — do not add node via + in a privacy list). " +
354
+ "A Contacts or Calendar grant does not include Messages.";
169
355
 
170
356
  /**
171
- * @param {"contacts"|"calendar"|string} source
357
+ * @param {"contacts"|"calendar"|"mail"|"messages"|string} source
172
358
  */
173
359
  export function tccGuidanceFor(source) {
174
360
  if (source === "contacts") return CONTACTS_TCC_GUIDANCE;
175
361
  if (source === "calendar") return CALENDAR_TCC_GUIDANCE;
362
+ if (source === "mail") return MAIL_TCC_GUIDANCE;
363
+ if (source === "messages") return MESSAGES_TCC_GUIDANCE;
176
364
  return TCC_GUIDANCE;
177
365
  }
178
366
 
@@ -184,9 +372,9 @@ export function tccGuidanceFor(source) {
184
372
  * @returns {{ ok: boolean, output: string, error: string|null, kind: string|null }}
185
373
  */
186
374
  export function runAppleScript(script, options = {}) {
187
- const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null } = options;
375
+ const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null, language = null } = options;
188
376
  try {
189
- const output = safeOsascript(script, { timeout });
377
+ const output = safeOsascript(script, { timeout, language });
190
378
  return { ok: true, output: (output || "").trim(), error: null, kind: null };
191
379
  } catch (e) {
192
380
  const message = e && e.message ? e.message : String(e);