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.
- package/README.md +78 -20
- package/index.js +30 -7
- package/indexer.js +4 -1
- package/lib/appleScript.js +209 -21
- package/lib/calendarWrite.js +835 -23
- package/lib/contactsWrite.js +192 -5
- package/lib/eventKitSession.js +369 -0
- package/lib/mailWrite.js +332 -26
- package/lib/messagesWrite.js +39 -3
- package/lib/permissions.js +360 -0
- package/lib/processMode.js +47 -0
- package/lib/shell.js +50 -5
- package/lib/writeGuards.js +8 -0
- package/lib/writeRouting.js +76 -6
- package/lib/writeTools.js +26 -8
- package/package.json +4 -1
- package/scripts/postinstall.js +32 -0
- package/scripts/smoke-writes.js +188 -17
package/lib/writeTools.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import { contactsAdd, contactsEdit, contactsRemove } from "./contactsWrite.js";
|
|
33
33
|
import { planWriteRoute, planAfterDelegation, tccFallbackAdvice } from "./writeRouting.js";
|
|
34
34
|
import { requestWriteViaBridge, probeSocket, defaultSocketPath } from "./writeBridge.js";
|
|
35
|
-
import {
|
|
35
|
+
import { needsHostTccAdvice } from "./appleScript.js";
|
|
36
36
|
|
|
37
37
|
const CONFIRM_PROPS = {
|
|
38
38
|
dry_run: { type: "boolean", description: "Preview only: report what would happen and change nothing (default false)" },
|
|
@@ -48,7 +48,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
48
48
|
// ============ MAIL WRITES ============
|
|
49
49
|
{
|
|
50
50
|
name: "mail_send",
|
|
51
|
-
description: "Send an email through Mail.app. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true. Use dry_run=true to preview.",
|
|
51
|
+
description: "Send an email through Mail.app. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true. Use dry_run=true to preview. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; the tool checks Sent before failing. Clients must Sent-check before retrying a timed-out send.",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: "object",
|
|
54
54
|
properties: {
|
|
@@ -82,7 +82,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: "mail_reply",
|
|
85
|
-
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true.",
|
|
85
|
+
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; the tool checks Sent before failing. Clients must Sent-check before retrying a timed-out send.",
|
|
86
86
|
inputSchema: {
|
|
87
87
|
type: "object",
|
|
88
88
|
properties: {
|
|
@@ -97,7 +97,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
name: "mail_forward",
|
|
100
|
-
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true.",
|
|
100
|
+
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; the tool checks Sent before failing. Clients must Sent-check before retrying a timed-out send.",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object",
|
|
103
103
|
properties: {
|
|
@@ -207,6 +207,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
207
207
|
type: "object",
|
|
208
208
|
properties: {
|
|
209
209
|
event_id: { type: "string", description: "Event ID from calendar_date or calendar_add (required)" },
|
|
210
|
+
eventkit_id: { type: "string", description: "EventKit eventIdentifier from calendar_add (via EventKit). Prefer this over Calendar.app uid lookup." },
|
|
210
211
|
title: { type: "string", description: "New title" },
|
|
211
212
|
start: { type: "string", description: "New start as YYYY-MM-DD HH:MM local time" },
|
|
212
213
|
end: { type: "string", description: "New end as YYYY-MM-DD HH:MM local time" },
|
|
@@ -231,7 +232,9 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
231
232
|
inputSchema: {
|
|
232
233
|
type: "object",
|
|
233
234
|
properties: {
|
|
234
|
-
event_id: { type: "string", description: "Event ID from calendar_date (required)" },
|
|
235
|
+
event_id: { type: "string", description: "Event ID from calendar_date or calendar_add (required)" },
|
|
236
|
+
eventkit_id: { type: "string", description: "Optional EventKit eventIdentifier returned by calendar_add (via EventKit). Needed so writeOnly EventKit can delete the event it created." },
|
|
237
|
+
calendar_name: { type: "string", description: "Optional calendar from calendar_list_calendars; narrows the AppleScript delete so Calendar does not scan every calendar" },
|
|
235
238
|
...CONFIRM_PROPS
|
|
236
239
|
},
|
|
237
240
|
required: ["event_id"]
|
|
@@ -327,6 +330,13 @@ export const WRITE_TOOL_HANDLERS = {
|
|
|
327
330
|
contacts_remove: contactsRemove
|
|
328
331
|
};
|
|
329
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Smoke-only helpers, never MCP write tools. CallTool uses isWriteTool, so
|
|
335
|
+
* these names must stay off WRITE_TOOL_HANDLERS or a client can invoke them
|
|
336
|
+
* without dry_run / confirm.
|
|
337
|
+
*/
|
|
338
|
+
export const SMOKE_ONLY_AUTOMATION_PROBES = ["mail_automation_probe", "messages_automation_probe"];
|
|
339
|
+
|
|
330
340
|
export const WRITE_TOOL_NAMES = Object.keys(WRITE_TOOL_HANDLERS);
|
|
331
341
|
|
|
332
342
|
export function isWriteTool(name) {
|
|
@@ -368,7 +378,7 @@ export async function dispatchWriteTool(name, args = {}, deps = {}) {
|
|
|
368
378
|
log = () => {}
|
|
369
379
|
} = deps;
|
|
370
380
|
|
|
371
|
-
if (!isWriteTool(name)) {
|
|
381
|
+
if (SMOKE_ONLY_AUTOMATION_PROBES.includes(name) || !isWriteTool(name)) {
|
|
372
382
|
return { ok: false, message: `Unknown write tool: ${name}` };
|
|
373
383
|
}
|
|
374
384
|
|
|
@@ -389,8 +399,16 @@ export async function dispatchWriteTool(name, args = {}, deps = {}) {
|
|
|
389
399
|
}
|
|
390
400
|
|
|
391
401
|
const result = runLocally(name, args);
|
|
392
|
-
if (
|
|
393
|
-
|
|
402
|
+
if (
|
|
403
|
+
result &&
|
|
404
|
+
result.ok === false &&
|
|
405
|
+
result.suppressTccAdvice !== true &&
|
|
406
|
+
needsHostTccAdvice(result.message)
|
|
407
|
+
) {
|
|
408
|
+
return {
|
|
409
|
+
...result,
|
|
410
|
+
message: `${result.message} ${tccFallbackAdvice({ bridgeAvailable, execPath: process.execPath })}`
|
|
411
|
+
};
|
|
394
412
|
}
|
|
395
413
|
return result;
|
|
396
414
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-tools-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -47,12 +47,15 @@
|
|
|
47
47
|
"lib/",
|
|
48
48
|
"scripts/audit-index.js",
|
|
49
49
|
"scripts/smoke-writes.js",
|
|
50
|
+
"scripts/postinstall.js",
|
|
50
51
|
"README.md",
|
|
51
52
|
"LICENSE"
|
|
52
53
|
],
|
|
53
54
|
"scripts": {
|
|
54
55
|
"start": "node index.js",
|
|
55
56
|
"indexer": "node index.js --mode=indexer",
|
|
57
|
+
"permissions": "node index.js permissions",
|
|
58
|
+
"postinstall": "node scripts/postinstall.js",
|
|
56
59
|
"build-index": "node -e \"import('./indexer.js').then(i=>i.rebuildIndex()).catch(e=>{console.error(e.message);process.exit(1)})\"",
|
|
57
60
|
"audit": "node scripts/audit-index.js",
|
|
58
61
|
"smoke:writes": "node scripts/smoke-writes.js",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print-only reminder after npm install / upgrade.
|
|
4
|
+
*
|
|
5
|
+
* Must not run GUI / TCC probes unattended: a LaunchAgent or headless
|
|
6
|
+
* npm cannot click Allow. The operator runs `apple-tools-mcp permissions`
|
|
7
|
+
* on the host UI.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function postinstallReminderText() {
|
|
11
|
+
return [
|
|
12
|
+
"apple-tools-mcp: after first install or upgrade, run permissions on the Mac UI",
|
|
13
|
+
"(with System Settings → Privacy & Security → Automation open):",
|
|
14
|
+
"",
|
|
15
|
+
" $(which node) $(which apple-tools-mcp) permissions",
|
|
16
|
+
" apple-tools-mcp permissions",
|
|
17
|
+
" npx apple-tools-mcp permissions",
|
|
18
|
+
"",
|
|
19
|
+
"Use the same node binary the product uses (process.execPath).",
|
|
20
|
+
"A shebang `apple-tools-mcp` can start a different node than the path you typed;",
|
|
21
|
+
"Allows attach to execPath. Re-run with that exact node if the command WARNs.",
|
|
22
|
+
"This reminder does not grant anything and does not run the probes."
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function printPostinstallReminder(write = console.log) {
|
|
27
|
+
write(postinstallReminderText());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (process.argv[1] && /postinstall\.js$/.test(process.argv[1])) {
|
|
31
|
+
printPostinstallReminder();
|
|
32
|
+
}
|
package/scripts/smoke-writes.js
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Write-tool smoke test for QA prove-out on a Node host (Mac Mini / LaunchAgent).
|
|
4
4
|
*
|
|
5
|
-
* Covers
|
|
6
|
-
* -
|
|
5
|
+
* Covers the Automation / privacy classes that gate this package's writes:
|
|
6
|
+
* - Mail compose (Automation → Mail.app). dry_run never talks to Mail, so
|
|
7
|
+
* this step runs a real `make new outgoing message` (then discards it).
|
|
8
|
+
* A hang or timeout is TCC / Automation denied, not "Mail.app missing".
|
|
9
|
+
* - Messages account/service lookup (Automation → Messages.app). Nothing
|
|
10
|
+
* is sent. A deny fails --apply the same way Mail does.
|
|
11
|
+
* - Contacts CRUD (AddressBook class, via Contacts.app). The write path
|
|
12
|
+
* launches Contacts.app before CRUD; keep Contacts, Mail, and Messages
|
|
13
|
+
* running on the host. Calendar does not need to stay open (EventKit).
|
|
7
14
|
* - Calendar CRUD (calendars class, via Calendar.app)
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* Mail, Messages, Contacts, and Calendar must all pass on the Node host —
|
|
17
|
+
* that is the ship gate. A missing grant for any one of those four apps
|
|
18
|
+
* fails --apply. Contacts and Calendar CRUD are expected to fail under a
|
|
19
|
+
* host app that holds neither entitlement - that is a documented host
|
|
20
|
+
* limitation, not a package failure. Mail and Messages are separate Apple
|
|
21
|
+
* Events targets: a Contacts/Calendar grant does not include them.
|
|
12
22
|
*
|
|
13
23
|
* Default is a dry run: no contact or event is created, edited, or deleted.
|
|
14
24
|
* It is not a no-op, though: it reads contacts from the AddressBook database
|
|
@@ -32,6 +42,8 @@
|
|
|
32
42
|
import { loadContacts, getContactStats } from "../contacts.js";
|
|
33
43
|
import { probeSocket, defaultSocketPath } from "../lib/writeBridge.js";
|
|
34
44
|
import { dispatchWriteTool } from "../lib/writeTools.js";
|
|
45
|
+
import { probeMailAutomation } from "../lib/mailWrite.js";
|
|
46
|
+
import { probeMessagesAutomation } from "../lib/messagesWrite.js";
|
|
35
47
|
import { isIndexerMode } from "../lib/processMode.js";
|
|
36
48
|
|
|
37
49
|
export function parseSmokeArgs(argv = []) {
|
|
@@ -92,6 +104,37 @@ export function extractEventId(message) {
|
|
|
92
104
|
return match ? match[1] : null;
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
export function extractEventKitId(message) {
|
|
108
|
+
const match = String(message || "").match(/eventkit_id:\s*(\S+)/);
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
return match[1].replace(/[.,;]+$/, "");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createdViaEventKit(message) {
|
|
114
|
+
const text = String(message || "");
|
|
115
|
+
return /via:\s*EventKit\b/i.test(text) && Boolean(extractEventKitId(text));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Prefer an On My Mac calendar so Calendar.app delete is reliable if EventKit
|
|
120
|
+
* remove still misses. EventKit add/remove is the primary path on iCloud
|
|
121
|
+
* (writeOnly can delete events it created). An explicit --calendar= wins.
|
|
122
|
+
*/
|
|
123
|
+
export function pickSmokeCalendar(calendars, explicitName) {
|
|
124
|
+
if (explicitName) {
|
|
125
|
+
return { name: explicitName, reason: `--calendar=${explicitName}` };
|
|
126
|
+
}
|
|
127
|
+
const writable = (calendars || []).filter((c) => c.writable);
|
|
128
|
+
const local = writable.filter((c) => c.local);
|
|
129
|
+
if (local[0]) {
|
|
130
|
+
return { name: local[0].name, reason: "On My Mac (EventKit sourceType local)" };
|
|
131
|
+
}
|
|
132
|
+
if (writable[0]) {
|
|
133
|
+
return { name: writable[0].name, reason: "first writable calendar (no local calendar listed)" };
|
|
134
|
+
}
|
|
135
|
+
return { name: "Calendar", reason: "fallback name Calendar" };
|
|
136
|
+
}
|
|
137
|
+
|
|
95
138
|
/**
|
|
96
139
|
* Listing calendars is a live Calendar.app call even during a dry run, so a
|
|
97
140
|
* refusal there says something about this host - but it is not a failure of
|
|
@@ -104,6 +147,67 @@ export function calendarListSeverity(apply) {
|
|
|
104
147
|
return apply ? "error" : "warning";
|
|
105
148
|
}
|
|
106
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Mail compose is a live Apple Events call (dry_run of mail_send never
|
|
152
|
+
* touches Mail). On --apply a deny fails the ship gate. On a dry run it is
|
|
153
|
+
* advisory, like calendar_list_calendars, so a refused prompt is WARN.
|
|
154
|
+
*
|
|
155
|
+
* @returns {"error"|"warning"}
|
|
156
|
+
*/
|
|
157
|
+
export function mailProbeSeverity(apply) {
|
|
158
|
+
return apply ? "error" : "warning";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function messagesProbeSeverity(apply) {
|
|
162
|
+
return mailProbeSeverity(apply);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* How smoke exercises Mail Apple Events.
|
|
167
|
+
*
|
|
168
|
+
* The compose-and-discard helper is smoke-script-only — not an MCP write
|
|
169
|
+
* tool. On --apply with the write bridge up, also run public mail_draft so
|
|
170
|
+
* launchd-owned node is fail-closed for Mail (same make new outgoing message
|
|
171
|
+
* verb; nothing is sent).
|
|
172
|
+
*
|
|
173
|
+
* @returns {{ useLocalHelper: boolean, useMailDraft: boolean, reason: string }}
|
|
174
|
+
*/
|
|
175
|
+
export function planMailSmokeTouch({ apply, daemonPath }) {
|
|
176
|
+
if (apply && daemonPath) {
|
|
177
|
+
return {
|
|
178
|
+
useLocalHelper: true,
|
|
179
|
+
useMailDraft: true,
|
|
180
|
+
reason: "local compose-and-discard helper plus mail_draft via the write bridge (live Mail Apple Events; nothing is sent)"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
useLocalHelper: true,
|
|
185
|
+
useMailDraft: false,
|
|
186
|
+
reason: "smoke-script compose-and-discard helper (live Mail Apple Events; nothing is sent). mail_send dry_run never touches Mail."
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* How smoke exercises Messages Apple Events.
|
|
192
|
+
* Smoke-script-only helper; this gate never sends and is not an MCP tool.
|
|
193
|
+
*
|
|
194
|
+
* @returns {{ useLocalHelper: boolean, reason: string }}
|
|
195
|
+
*/
|
|
196
|
+
export function planMessagesSmokeTouch() {
|
|
197
|
+
return {
|
|
198
|
+
useLocalHelper: true,
|
|
199
|
+
reason: "smoke-script Messages account lookup (live Apple Events; nothing is sent). messages_send dry_run never touches Messages."
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function mailDraftSmokeArgs(stamp) {
|
|
204
|
+
return {
|
|
205
|
+
to: [`atm-mail-probe-${stamp}@example.com`],
|
|
206
|
+
subject: `ATM Mail Automation probe ${stamp}`,
|
|
207
|
+
body: "Created by apple-tools-mcp smoke test; safe to delete from Drafts."
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
107
211
|
/**
|
|
108
212
|
* A start/end pair well in the future, so a smoke event never collides with
|
|
109
213
|
* anything real on the calendar.
|
|
@@ -146,7 +250,7 @@ async function main() {
|
|
|
146
250
|
console.log("apple-tools-mcp write smoke test");
|
|
147
251
|
console.log("=".repeat(60));
|
|
148
252
|
line("Mode:", apply
|
|
149
|
-
? "APPLY (will change Contacts and Calendar)"
|
|
253
|
+
? "APPLY (will change Contacts and Calendar; Mail compose is live, nothing is sent)"
|
|
150
254
|
: "DRY RUN (no creates, edits, or deletes)");
|
|
151
255
|
line("Process:", indexerMode ? "indexer daemon" : "plain node / stdio");
|
|
152
256
|
line("Write bridge:", bridgeUp ? `listening at ${socketPath}` : `not listening (${socketPath})`);
|
|
@@ -162,13 +266,18 @@ async function main() {
|
|
|
162
266
|
"\nTCC note: macOS attributes this work to the process responsible for it.\n" +
|
|
163
267
|
"Contact reads use sqlite + Full Disk Access; the CRUD steps use Contacts.app\n" +
|
|
164
268
|
"and Calendar.app, gated by the AddressBook and calendars privacy classes.\n" +
|
|
269
|
+
"Mail compose is a separate Automation target (node → Mail.app). A hang or\n" +
|
|
270
|
+
"timeout there is TCC / Automation denied, not Mail.app missing.\n" +
|
|
165
271
|
`Writes take the same route production MCP clients take: ${route.reason}.`
|
|
166
272
|
);
|
|
167
273
|
if (!apply) {
|
|
168
274
|
console.log(
|
|
169
|
-
"Dry run:
|
|
275
|
+
"Dry run: no creates, edits, or deletes for Contacts/Calendar. calendar_list_calendars\n" +
|
|
170
276
|
"still runs for real - it is a live Calendar.app query and a TCC touch -\n" +
|
|
171
|
-
"so a denial there is reported as a warning, not a failure
|
|
277
|
+
"so a denial there is reported as a warning, not a failure.\n" +
|
|
278
|
+
"The Mail step also runs for real (make new outgoing message) because mail_send\n" +
|
|
279
|
+
"dry_run never touches Mail. A Mail deny is TCC / Automation denied; on a dry\n" +
|
|
280
|
+
"run it is a warning, on --apply it fails the ship gate."
|
|
172
281
|
);
|
|
173
282
|
}
|
|
174
283
|
|
|
@@ -192,7 +301,44 @@ async function main() {
|
|
|
192
301
|
const common = apply ? { confirm: true } : { dry_run: true };
|
|
193
302
|
const results = [];
|
|
194
303
|
|
|
304
|
+
// Mail first so first-run Allow includes Mail in the same pass as Contacts/Calendar.
|
|
305
|
+
// Helpers are smoke-script-only — not MCP write tools. mail_send dry_run never talks to Mail.
|
|
306
|
+
console.log("\n--- Mail Automation (Mail.app compose / Apple Events) ---");
|
|
307
|
+
if (!apply) {
|
|
308
|
+
console.log(" (live make new outgoing message even on a dry run; mail_send dry_run never touches Mail)");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const mailPlan = planMailSmokeTouch({ apply, daemonPath: route.path === "daemon" });
|
|
312
|
+
const mailSeverity = mailProbeSeverity(apply);
|
|
313
|
+
const mailHelperResult = probeMailAutomation();
|
|
314
|
+
if (mailHelperResult.ok === false && mailSeverity === "warning") {
|
|
315
|
+
step("mail Automation compose", mailHelperResult, "warning");
|
|
316
|
+
console.log(" Warning only: mail_send dry_run never touches Mail. --apply fails closed if Mail Automation is still denied.");
|
|
317
|
+
} else {
|
|
318
|
+
results.push(step("mail Automation compose", mailHelperResult, mailSeverity));
|
|
319
|
+
}
|
|
320
|
+
if (mailPlan.useMailDraft) {
|
|
321
|
+
console.log(` ${mailPlan.reason}`);
|
|
322
|
+
results.push(step("mail_draft (daemon Mail Automation)", await run("mail_draft", mailDraftSmokeArgs(stamp))));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log("\n--- Messages Automation (Messages.app accounts / Apple Events) ---");
|
|
326
|
+
if (!apply) {
|
|
327
|
+
console.log(" (live Messages account lookup even on a dry run; messages_send dry_run never touches Messages; nothing is sent)");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const messagesSeverity = messagesProbeSeverity(apply);
|
|
331
|
+
const messagesResult = probeMessagesAutomation();
|
|
332
|
+
|
|
333
|
+
if (messagesResult.ok === false && messagesSeverity === "warning") {
|
|
334
|
+
step("messages Automation lookup", messagesResult, "warning");
|
|
335
|
+
console.log(" Warning only: messages_send dry_run never touches Messages. --apply fails closed if Messages Automation is still denied.");
|
|
336
|
+
} else {
|
|
337
|
+
results.push(step("messages Automation lookup", messagesResult, messagesSeverity));
|
|
338
|
+
}
|
|
339
|
+
|
|
195
340
|
console.log("\n--- Contacts CRUD (Contacts.app / AddressBook privacy class) ---");
|
|
341
|
+
console.log(" Keep Contacts, Mail, and Messages running for write reliability. Calendar does not need to stay open (EventKit).");
|
|
196
342
|
const created = step("contacts_add", await run("contacts_add", {
|
|
197
343
|
first_name: "ATM Smoke",
|
|
198
344
|
last_name: `Test ${stamp}`,
|
|
@@ -240,11 +386,12 @@ async function main() {
|
|
|
240
386
|
}
|
|
241
387
|
|
|
242
388
|
const writable = (calendars.calendars || []).filter((c) => c.writable);
|
|
243
|
-
const
|
|
389
|
+
const picked = pickSmokeCalendar(calendars.calendars || [], calendar);
|
|
390
|
+
const targetCalendar = picked.name;
|
|
244
391
|
if (calendar && writable.length > 0 && !writable.some((c) => c.name === calendar)) {
|
|
245
392
|
console.log(` Warning: --calendar=${calendar} is not in the writable list; trying it anyway.`);
|
|
246
393
|
}
|
|
247
|
-
line(" Target calendar:", targetCalendar);
|
|
394
|
+
line(" Target calendar:", `${targetCalendar} (${picked.reason})`);
|
|
248
395
|
|
|
249
396
|
const window = smokeEventWindow();
|
|
250
397
|
const eventCreated = step("calendar_add", await run("calendar_add", {
|
|
@@ -260,12 +407,19 @@ async function main() {
|
|
|
260
407
|
|
|
261
408
|
if (eventCreated.ok !== false) {
|
|
262
409
|
const eventId = apply ? extractEventId(eventCreated.message) : "ATM-SMOKE-EVENT-UID";
|
|
263
|
-
|
|
410
|
+
const extractedKitId = apply ? extractEventKitId(eventCreated.message) : "EK-SMOKE";
|
|
411
|
+
const eventKitId = extractedKitId || (eventId && eventId.includes(":") ? eventId : null);
|
|
412
|
+
if (apply && !createdViaEventKit(eventCreated.message)) {
|
|
413
|
+
console.log(" calendar_add did not print via: EventKit and eventkit_id.");
|
|
414
|
+
console.log(" Ship-gate refuses a Calendar.app-only create: writeOnly EventKit cannot delete those events, and Calendar.app delete hangs.");
|
|
415
|
+
results.push({ ok: false, message: "calendar_add ship-gate requires via: EventKit and eventkit_id" });
|
|
416
|
+
} else if (apply && !eventId) {
|
|
264
417
|
console.log(" calendar_add reported success but returned no event id; skipping edit/delete.");
|
|
265
418
|
results.push({ ok: false });
|
|
266
419
|
} else {
|
|
267
420
|
results.push(step("calendar_edit", await run("calendar_edit", {
|
|
268
421
|
event_id: eventId,
|
|
422
|
+
eventkit_id: eventKitId || undefined,
|
|
269
423
|
title: `ATM smoke test ${stamp} (edited)`,
|
|
270
424
|
...common
|
|
271
425
|
})));
|
|
@@ -273,10 +427,25 @@ async function main() {
|
|
|
273
427
|
if (apply && keep) {
|
|
274
428
|
console.log(` Left event ${eventId} in place (--keep). Delete it when you are done.`);
|
|
275
429
|
} else {
|
|
276
|
-
const removed = step("calendar_remove", await run("calendar_remove", {
|
|
430
|
+
const removed = step("calendar_remove", await run("calendar_remove", {
|
|
431
|
+
event_id: eventId,
|
|
432
|
+
eventkit_id: eventKitId || undefined,
|
|
433
|
+
calendar_name: targetCalendar,
|
|
434
|
+
...common
|
|
435
|
+
}));
|
|
277
436
|
results.push(removed);
|
|
278
437
|
if (apply && removed.ok === false) {
|
|
279
|
-
console.log(` Created ${eventId} but could not delete it
|
|
438
|
+
console.log(` Created ${eventId} on ${targetCalendar} but could not delete it.`);
|
|
439
|
+
console.log(" add/edit succeeded, so this is a delete-path failure, not missing Calendar Automation.");
|
|
440
|
+
console.log(" Same write-bridge RPC as add/edit; Calendar.app has no remove/move-to-trash (delete + EventKit fallback).");
|
|
441
|
+
if (removed.diagnostics) {
|
|
442
|
+
console.log(` ${removed.diagnostics}`);
|
|
443
|
+
} else {
|
|
444
|
+
const rawLine = String(removed.message || "").match(/osascript kind=\S+ error=.*/);
|
|
445
|
+
if (rawLine) console.log(` ${rawLine[0]}`);
|
|
446
|
+
}
|
|
447
|
+
console.log(" Paste the osascript kind=/error= line if asking for another tip.");
|
|
448
|
+
console.log(" Remove the leftover event in Calendar.app.");
|
|
280
449
|
}
|
|
281
450
|
}
|
|
282
451
|
}
|
|
@@ -293,14 +462,16 @@ async function main() {
|
|
|
293
462
|
} else {
|
|
294
463
|
console.log("These writes ran inside the indexer daemon, so this is a real gate failure:");
|
|
295
464
|
console.log("grant the daemon's node binary Full Disk Access (reads) and Allow node in");
|
|
296
|
-
console.log("System Settings > Privacy & Security > Automation for
|
|
297
|
-
console.log("Calendar.app.
|
|
465
|
+
console.log("System Settings > Privacy & Security > Automation for Mail.app, Messages.app,");
|
|
466
|
+
console.log("Contacts.app, and Calendar.app. A hang or timeout on Mail compose is TCC /");
|
|
467
|
+
console.log("Automation denied, not Mail.app missing. Do not add node via + in the");
|
|
468
|
+
console.log("Contacts or Calendars privacy lists.");
|
|
298
469
|
}
|
|
299
470
|
process.exitCode = 1;
|
|
300
471
|
} else if (apply) {
|
|
301
|
-
console.log(`Result: PASS - Contacts and Calendar
|
|
472
|
+
console.log(`Result: PASS - Mail, Messages, Contacts, and Calendar Automation all work (${route.path === "daemon" ? "via the write bridge" : "in this process"}).`);
|
|
302
473
|
} else {
|
|
303
|
-
console.log("Result: PASS - dry run only. Re-run with --apply to prove real CRUD.");
|
|
474
|
+
console.log("Result: PASS - dry run only. Re-run with --apply to prove real CRUD and fail-closed Mail/Messages/Contacts/Calendar Automation.");
|
|
304
475
|
}
|
|
305
476
|
}
|
|
306
477
|
|