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.
@@ -15,11 +15,15 @@ import {
15
15
  parseWriteDateTime,
16
16
  DATE_HANDLER,
17
17
  CALENDAR_TCC_GUIDANCE,
18
- ATTRIBUTION_GUIDANCE
18
+ CALENDAR_APP_NOT_RUNNING_GUIDANCE,
19
+ ATTRIBUTION_GUIDANCE,
20
+ formatOsascriptDiagnostic,
21
+ isHardTccDenial
19
22
  } from "./appleScript.js";
20
23
  import {
21
24
  planWrite,
22
25
  validateEventId,
26
+ validateEventKitId,
23
27
  validateCalendarName,
24
28
  validateBody,
25
29
  validateSubject,
@@ -29,6 +33,11 @@ import {
29
33
  normalizeList,
30
34
  truncate
31
35
  } from "./writeGuards.js";
36
+ import {
37
+ ensureEventKitSession,
38
+ getEventKitSession,
39
+ setEventKitSession
40
+ } from "./eventKitSession.js";
32
41
 
33
42
  export const RECURRENCE_FREQUENCIES = ["daily", "weekly", "monthly", "yearly"];
34
43
  export const RECURRENCE_DAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
@@ -176,16 +185,19 @@ function alarmLines(minutes, eventVar) {
176
185
  }
177
186
 
178
187
  function failure(action, summary, result, secrets = []) {
179
- if (result.kind === "tcc") {
188
+ if (result.kind === "tcc" || result.kind === "timeout") {
180
189
  return `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
181
190
  }
182
191
  const raw = String(result.error || "");
183
192
  if (raw.includes("CALENDAR_NOT_FOUND")) {
184
193
  return `${action} failed — attempted to ${summary}. That calendar does not exist; call calendar_list_calendars first.`;
185
194
  }
186
- if (raw.includes("EVENT_NOT_FOUND")) {
195
+ if (result.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
187
196
  return `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
188
197
  }
198
+ if (result.kind === "app_not_running") {
199
+ return `${action} failed — attempted to ${summary}. ${CALENDAR_APP_NOT_RUNNING_GUIDANCE}`;
200
+ }
189
201
  if (result.kind === "attribution") {
190
202
  return `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
191
203
  }
@@ -195,6 +207,435 @@ function failure(action, summary, result, secrets = []) {
195
207
  return writeErrorMessage(action, summary, new Error(raw || "unknown error"), secrets);
196
208
  }
197
209
 
210
+ /**
211
+ * calendar_remove must never hide the AppleEvent code behind TCC copy.
212
+ * Mini --apply: add/edit PASS then remove FAIL looked like a TCC deny
213
+ * because failure() dropped stderr when classify mapped ETIMEDOUT/-1712
214
+ * to tcc. Same write-bridge RPC as add/edit; not a different calendar
215
+ * account. Calendar.app has no `remove` / `move to trash` — only `delete`.
216
+ */
217
+ export function describeCalendarRemoveFailure(action, summary, appleResult, eventKitResult = null) {
218
+ const parts = [`AppleScript ${formatOsascriptDiagnostic(appleResult, "osascript")}`];
219
+ if (eventKitResult) {
220
+ parts.push(`EventKit ${formatOsascriptDiagnostic(eventKitResult, "osascript")}`);
221
+ }
222
+ const diagnostics = parts.join(" ");
223
+ const raw = String(appleResult.error || "");
224
+ const hardTcc = isHardTccDenial(raw);
225
+ const timedOut =
226
+ appleResult.kind === "timeout" ||
227
+ /delete_timeout|etimedout|-1712|appleevent timed out|timed out after/i.test(raw);
228
+
229
+ let head;
230
+ if (raw.includes("CALENDAR_NOT_FOUND")) {
231
+ head = `${action} failed — attempted to ${summary}. That calendar does not exist; call calendar_list_calendars first.`;
232
+ } else if (hardTcc) {
233
+ head = `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
234
+ } else if (timedOut) {
235
+ head = `${action} failed — attempted to ${summary}. Calendar.app delete timed out. That is not a TCC / Automation deny when calendar_add/calendar_edit succeed on this host — Calendar.app has no remove or move-to-trash; iCloud/CalDAV delete can hang or wait on a confirmation dialog.`;
236
+ } else if (appleResult.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
237
+ head = `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
238
+ } else if (appleResult.kind === "app_not_running") {
239
+ head = `${action} failed — attempted to ${summary}. ${CALENDAR_APP_NOT_RUNNING_GUIDANCE}`;
240
+ } else if (appleResult.kind === "attribution") {
241
+ head = `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
242
+ } else if (appleResult.kind === "app_unavailable") {
243
+ head = `${action} failed — attempted to ${summary}. Calendar.app could not be reached on this host.`;
244
+ } else {
245
+ head = writeErrorMessage(action, summary, new Error(raw || "unknown error"));
246
+ }
247
+
248
+ return {
249
+ ok: false,
250
+ message: `${head} ${diagnostics}`,
251
+ suppressTccAdvice: !hardTcc,
252
+ diagnostics
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Non-recurring add must not fall through to Calendar.app: Mini writeOnly
258
+ * EventKit cannot delete AppleScript-created events, and Calendar.app
259
+ * delete hangs (ETIMEDOUT). Fail closed with the EventKit code.
260
+ */
261
+ export function describeCalendarAddFailure(action, summary, eventKitResult) {
262
+ const diagnostics = `EventKit ${formatOsascriptDiagnostic(eventKitResult)}`;
263
+ const raw = String(eventKitResult && eventKitResult.error ? eventKitResult.error : "");
264
+ const hardTcc = isHardTccDenial(raw) || /eventkit_denied/i.test(raw);
265
+ let head;
266
+ if (raw.includes("CALENDAR_NOT_FOUND")) {
267
+ head = `${action} failed — attempted to ${summary}. EventKit could not target that calendar (writeOnly often cannot list calendars; we use defaultCalendarForNewEvents). Call calendar_list_calendars first.`;
268
+ } else if (hardTcc) {
269
+ head = `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
270
+ } else {
271
+ head = `${action} failed — attempted to ${summary}. EventKit create failed; non-recurring add does not fall back to Calendar.app because writeOnly EventKit cannot see AppleScript-created events and Calendar.app delete hangs.`;
272
+ }
273
+ return {
274
+ ok: false,
275
+ message: `${head} ${diagnostics}`,
276
+ suppressTccAdvice: !hardTcc,
277
+ diagnostics
278
+ };
279
+ }
280
+
281
+ export function describeEventKitWriteFailure(action, summary, eventKitResult) {
282
+ const diagnostics = `EventKit ${formatOsascriptDiagnostic(eventKitResult)}`;
283
+ const raw = String(eventKitResult && eventKitResult.error ? eventKitResult.error : "");
284
+ const hardTcc = isHardTccDenial(raw) || /eventkit_denied/i.test(raw);
285
+ const head = raw.includes("EVENTKIT_NOT_FOUND")
286
+ ? `${action} failed — attempted to ${summary}. writeOnly EventKit cannot re-query by id; edit/remove need the in-memory EKEvent from the same EventKit session that created it. Calendar.app uid lookup was skipped to avoid an iCloud hang.`
287
+ : `${action} failed — attempted to ${summary}. EventKit write failed; Calendar.app was not used because eventkit_id was provided.`;
288
+ return {
289
+ ok: false,
290
+ message: `${head} ${diagnostics}`,
291
+ suppressTccAdvice: !hardTcc,
292
+ diagnostics
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Mini 74d304e: EventKit status=4 is writeOnly. That grant can create and
298
+ * delete events *this process* saved, but calendarItemsWithExternalIdentifier
299
+ * cannot see an event Calendar.app created via AppleScript. Align add→remove
300
+ * by creating through EventKit and deleting with the same identifiers
301
+ * (calendarItemExternalIdentifier and eventIdentifier).
302
+ */
303
+ export function localUnixSeconds(parts) {
304
+ return Math.floor(new Date(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0).getTime() / 1000);
305
+ }
306
+
307
+ export function parseEventKitAddOutput(output) {
308
+ const text = String(output || "").trim();
309
+ if (!text) return { eventId: null, eventKitId: null, calendar: null };
310
+ const [externalId, eventKitId, calendar, calendarItemId] = text.split("<<>>");
311
+ const ext = String(externalId || "").trim();
312
+ const local = String(eventKitId || "").trim();
313
+ const item = String(calendarItemId || "").trim();
314
+ return {
315
+ eventId: ext || local || item || null,
316
+ eventKitId: local || item || null,
317
+ calendar: String(calendar || "").trim() || null
318
+ };
319
+ }
320
+
321
+ export function parseEventKitCalendarList(output) {
322
+ return String(output || "")
323
+ .split("|||")
324
+ .map((entry) => entry.trim())
325
+ .filter((entry) => entry.length > 0)
326
+ .map((entry) => {
327
+ const parts = entry.split("<<>>");
328
+ if (parts.length < 3) return null;
329
+ const [name, writable, local, sourceType, source] = parts;
330
+ return {
331
+ name: name || "",
332
+ writable: writable !== "no",
333
+ local: local === "yes",
334
+ sourceType: sourceType === undefined || sourceType === "" ? null : Number(sourceType),
335
+ source: source || ""
336
+ };
337
+ })
338
+ .filter(Boolean);
339
+ }
340
+
341
+ /**
342
+ * JXA prints NSString as `[id NSTaggedPointerString]` if you `String(title)`.
343
+ * Mini cd74071: default=[id NSTaggedPointerString] so title match failed
344
+ * even though eventKitCalendars=1. Always ObjC.unwrap / .js.
345
+ */
346
+ export const EVENTKIT_JXA_HELPERS = `function jsString(value) {
347
+ if (value === undefined || value === null) return "";
348
+ if (typeof value === "string") {
349
+ if (value.indexOf("[id ") === 0 || value === "[object Object]") return "";
350
+ return value;
351
+ }
352
+ try {
353
+ if (typeof value.js === "string") return value.js;
354
+ } catch (e) {}
355
+ try {
356
+ var unwrapped = ObjC.unwrap(value);
357
+ if (typeof unwrapped === "string") return unwrapped;
358
+ } catch (e) {}
359
+ return "";
360
+ }
361
+ function titleOf(c) {
362
+ try { return c ? jsString(c.title) : ""; } catch (e) { return ""; }
363
+ }
364
+ function identifierOf(c) {
365
+ try { return c ? jsString(c.calendarIdentifier) : ""; } catch (e) { return ""; }
366
+ }
367
+ function asEvent(item) {
368
+ if (item === undefined || item === null) return false;
369
+ try { if (item.isKindOfClass && item.isKindOfClass($.EKEvent)) return true; } catch (e) {}
370
+ try { if (jsString(item.eventIdentifier)) return true; } catch (e) {}
371
+ try { if (jsString(item.calendarItemExternalIdentifier)) return true; } catch (e) {}
372
+ try { if (jsString(item.calendarItemIdentifier)) return true; } catch (e) {}
373
+ return false;
374
+ }
375
+ function nsId(id) {
376
+ return $.NSString.stringWithString(String(id));
377
+ }
378
+ function firstMatching(items) {
379
+ if (!items) return null;
380
+ var n = 0;
381
+ try { n = Number(items.count); } catch (e) { n = 0; }
382
+ for (var j = 0; j < n; j++) {
383
+ var hit = null;
384
+ try { hit = items.objectAtIndex(j); } catch (e) {}
385
+ if (asEvent(hit)) return hit;
386
+ }
387
+ return null;
388
+ }
389
+ function lookupOne(store, raw) {
390
+ if (!raw) return null;
391
+ var bridged = nsId(raw);
392
+ var candidates = [bridged, raw];
393
+ for (var k = 0; k < candidates.length; k++) {
394
+ var id = candidates[k];
395
+ var ev = null;
396
+ try { ev = store.eventWithIdentifier(id); } catch (e) {}
397
+ if (!asEvent(ev)) {
398
+ try { ev = store.eventWithIdentifier_(id); } catch (e) {}
399
+ }
400
+ if (asEvent(ev)) return ev;
401
+ try {
402
+ var item = store.calendarItemWithIdentifier(id);
403
+ if (asEvent(item)) return item;
404
+ } catch (e) {}
405
+ try {
406
+ var item2 = store.calendarItemWithIdentifier_(id);
407
+ if (asEvent(item2)) return item2;
408
+ } catch (e) {}
409
+ try {
410
+ var found = firstMatching(store.calendarItemsWithExternalIdentifier(id));
411
+ if (found) return found;
412
+ } catch (e) {}
413
+ try {
414
+ var found2 = firstMatching(store.calendarItemsWithExternalIdentifier_(id));
415
+ if (found2) return found2;
416
+ } catch (e) {}
417
+ }
418
+ return null;
419
+ }
420
+ function findEventByIds(store, ids) {
421
+ try { store.refreshSourcesIfNecessary(); } catch (e) {}
422
+ for (var i = 0; i < ids.length; i++) {
423
+ var found = lookupOne(store, ids[i]);
424
+ if (found) return found;
425
+ }
426
+ return null;
427
+ }
428
+ `;
429
+
430
+ /**
431
+ * Mini ed6d834: eventkit_id is `calendarUUID:eventUUID`. Look up the
432
+ * compound id and each half so eventWithIdentifier / externalIdentifier
433
+ * both get a usable string.
434
+ */
435
+ export function eventKitLookupIds(eventId, eventKitId) {
436
+ const out = [];
437
+ const seen = new Set();
438
+ const add = (value) => {
439
+ if (typeof value !== "string") return;
440
+ const trimmed = value.trim();
441
+ if (!trimmed || seen.has(trimmed)) return;
442
+ seen.add(trimmed);
443
+ out.push(trimmed);
444
+ };
445
+ add(eventKitId);
446
+ add(eventId);
447
+ for (const value of [...out]) {
448
+ if (value.includes(":")) {
449
+ for (const part of value.split(":")) add(part);
450
+ }
451
+ if (value.includes("/")) add(value.split("/")[0]);
452
+ }
453
+ return out;
454
+ }
455
+
456
+ function eventKitSessionRequest(cmd) {
457
+ const session = getEventKitSession() || ensureEventKitSession({ helpers: EVENTKIT_JXA_HELPERS });
458
+ if (!session) return null;
459
+ try {
460
+ return session.request(cmd);
461
+ } catch (e) {
462
+ try { session.close(); } catch { /* ignore */ }
463
+ setEventKitSession(null);
464
+ return { ok: false, error: e && e.message ? e.message : String(e), sessionDead: true };
465
+ }
466
+ }
467
+
468
+ export function mergeCalendarSources(appleCalendars, eventKitCalendars) {
469
+ if (!eventKitCalendars || eventKitCalendars.length === 0) return appleCalendars || [];
470
+ const byName = new Map(eventKitCalendars.map((c) => [c.name, c]));
471
+ return (appleCalendars || []).map((c) => {
472
+ const ek = byName.get(c.name);
473
+ return {
474
+ ...c,
475
+ local: ek ? ek.local : false,
476
+ sourceType: ek ? ek.sourceType : null,
477
+ source: ek ? ek.source : null
478
+ };
479
+ });
480
+ }
481
+
482
+ export function buildEventKitListCalendarsScript() {
483
+ return `ObjC.import("EventKit");
484
+ ObjC.import("Foundation");
485
+ ${EVENTKIT_JXA_HELPERS}
486
+ var store = $.EKEventStore.alloc.init;
487
+ var rows = [];
488
+ var seen = {};
489
+ function addRow(c) {
490
+ if (!c) return;
491
+ var name = titleOf(c);
492
+ if (!name || seen[name]) return;
493
+ seen[name] = true;
494
+ var src = null;
495
+ try { src = c.source; } catch (e) {}
496
+ var type = -1;
497
+ try { if (src) type = Number(src.sourceType); } catch (e) {}
498
+ var srcName = "";
499
+ try { if (src) srcName = jsString(src.title); } catch (e) {}
500
+ var writable = "yes";
501
+ try { if (c.allowsContentModifications === false) writable = "no"; } catch (e) {}
502
+ var local = type === 0 ? "yes" : "no";
503
+ rows.push(name + "<<>>" + writable + "<<>>" + local + "<<>>" + type + "<<>>" + srcName);
504
+ }
505
+ try {
506
+ var cals = store.calendarsForEntityType($.EKEntityTypeEvent);
507
+ for (var i = 0; i < cals.count; i++) addRow(cals.objectAtIndex(i));
508
+ } catch (e) {}
509
+ try { addRow(store.defaultCalendarForNewEvents); } catch (e) {}
510
+ rows.join("|||");`;
511
+ }
512
+
513
+ export function buildEventKitAddScript({ calendarName, title, start, end, allDay, location, notes, alerts }) {
514
+ const startSec = localUnixSeconds(start);
515
+ const endSec = localUnixSeconds(end);
516
+ return `ObjC.import("EventKit");
517
+ ObjC.import("Foundation");
518
+ ${EVENTKIT_JXA_HELPERS}
519
+ var calendarName = ${JSON.stringify(calendarName)};
520
+ var title = ${JSON.stringify(title)};
521
+ var startSec = ${asInteger(startSec, { field: "start" })};
522
+ var endSec = ${asInteger(endSec, { field: "end" })};
523
+ var allDay = ${allDay ? "true" : "false"};
524
+ var location = ${JSON.stringify(location || "")};
525
+ var notes = ${JSON.stringify(notes || "")};
526
+ var alerts = ${JSON.stringify(alerts || [])};
527
+ var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
528
+ if (status === 1 || status === 2) {
529
+ throw new Error("EVENTKIT_DENIED status=" + status);
530
+ }
531
+ var store = $.EKEventStore.alloc.init;
532
+ var defaultCal = null;
533
+ try { defaultCal = store.defaultCalendarForNewEvents; } catch (e) {}
534
+ var cals = null;
535
+ var count = 0;
536
+ try {
537
+ cals = store.calendarsForEntityType($.EKEntityTypeEvent);
538
+ count = cals ? Number(cals.count) : 0;
539
+ } catch (e) {}
540
+ var target = null;
541
+ var writables = [];
542
+ var wanted = String(calendarName || "").toLowerCase();
543
+ function matchesWanted(c) {
544
+ var t = titleOf(c).toLowerCase();
545
+ var id = identifierOf(c);
546
+ return (t && t === wanted) || (id && (id === calendarName || id.toLowerCase() === wanted));
547
+ }
548
+ for (var i = 0; i < count; i++) {
549
+ var c = cals.objectAtIndex(i);
550
+ var writable = true;
551
+ try { writable = !!c.allowsContentModifications; } catch (e) {}
552
+ if (!writable) continue;
553
+ writables.push(c);
554
+ if (matchesWanted(c)) {
555
+ target = c;
556
+ break;
557
+ }
558
+ }
559
+ if (!target && defaultCal && matchesWanted(defaultCal)) {
560
+ target = defaultCal;
561
+ }
562
+ if (!target && writables.length === 1) {
563
+ target = writables[0];
564
+ }
565
+ if (!target && defaultCal) {
566
+ target = defaultCal;
567
+ }
568
+ if (!target) {
569
+ throw new Error("CALENDAR_NOT_FOUND status=" + status + " eventKitCalendars=" + count + " default=" + titleOf(defaultCal) + " defaultId=" + identifierOf(defaultCal));
570
+ }
571
+ var event = null;
572
+ try { event = $.EKEvent.eventWithEventStore(store); } catch (e) {}
573
+ if (!event) {
574
+ try { event = $.EKEvent.alloc.initWithEventStore(store); } catch (e2) {}
575
+ }
576
+ if (!event) throw new Error("EVENTKIT_NO_EVENT status=" + status);
577
+ event.title = title;
578
+ event.startDate = $.NSDate.dateWithTimeIntervalSince1970(startSec);
579
+ event.endDate = $.NSDate.dateWithTimeIntervalSince1970(endSec);
580
+ event.allDay = allDay;
581
+ try { event.calendar = target; } catch (e) {}
582
+ try { if (event.setCalendar) event.setCalendar(target); } catch (e) {}
583
+ if (location) event.location = location;
584
+ if (notes) event.notes = notes;
585
+ if (alerts && alerts.length) {
586
+ var alarms = $.NSMutableArray.array;
587
+ for (var a = 0; a < alerts.length; a++) {
588
+ alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(alerts[a]) * 60));
589
+ }
590
+ event.alarms = alarms;
591
+ }
592
+ var err = Ref();
593
+ var ok = false;
594
+ try {
595
+ ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
596
+ } catch (e) {
597
+ throw new Error("EVENTKIT_SAVE_FAILED: " + String(e));
598
+ }
599
+ if (!ok) {
600
+ var desc = "unknown";
601
+ try { desc = String(err[0]); } catch (e) {}
602
+ throw new Error("EVENTKIT_SAVE_FAILED: " + desc + " status=" + status);
603
+ }
604
+ var externalId = "";
605
+ var localId = "";
606
+ var itemId = "";
607
+ try { externalId = jsString(event.calendarItemExternalIdentifier); } catch (e) {}
608
+ try { localId = jsString(event.eventIdentifier); } catch (e) {}
609
+ try { itemId = jsString(event.calendarItemIdentifier); } catch (e) {}
610
+ if (!externalId && !localId && !itemId) throw new Error("EVENTKIT_NO_ID status=" + status);
611
+ externalId + "<<>>" + localId + "<<>>" + titleOf(target) + "<<>>" + itemId;`;
612
+ }
613
+
614
+ export function buildEventKitRemoveScript(eventId, { eventKitId = null } = {}) {
615
+ const ids = eventKitLookupIds(eventId, eventKitId);
616
+ return `ObjC.import("EventKit");
617
+ ObjC.import("Foundation");
618
+ ${EVENTKIT_JXA_HELPERS}
619
+ var ids = ${JSON.stringify(ids)};
620
+ var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
621
+ if (status === 1 || status === 2) {
622
+ throw new Error("EVENTKIT_DENIED status=" + status);
623
+ }
624
+ var store = $.EKEventStore.alloc.init;
625
+ var found = findEventByIds(store, ids);
626
+ if (!found) {
627
+ throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " tried=" + ids.join(","));
628
+ }
629
+ var err = Ref();
630
+ var ok = store.removeEventSpanCommitError(found, $.EKSpanThisEvent, true, err);
631
+ if (!ok) {
632
+ var desc = "unknown";
633
+ try { desc = String(err[0]); } catch (e) {}
634
+ throw new Error("EVENTKIT_REMOVE_FAILED: " + desc);
635
+ }
636
+ 1;`;
637
+ }
638
+
198
639
  /**
199
640
  * List calendars so a caller can pick a target instead of defaulting.
200
641
  */
@@ -221,7 +662,7 @@ return outputList as string`;
221
662
  return { ok: false, message: failure(action, "list calendars", result) };
222
663
  }
223
664
 
224
- const calendars = result.output
665
+ let calendars = result.output
225
666
  .split("|||")
226
667
  .map((entry) => entry.trim())
227
668
  .filter((entry) => entry.length > 0)
@@ -230,11 +671,26 @@ return outputList as string`;
230
671
  return { name: name || "", writable: writable !== "no" };
231
672
  });
232
673
 
674
+ const ekList = runAppleScript(buildEventKitListCalendarsScript(), {
675
+ timeout: 15000,
676
+ appName: "Calendar",
677
+ language: "JavaScript"
678
+ });
679
+ if (ekList.ok) {
680
+ calendars = mergeCalendarSources(calendars, parseEventKitCalendarList(ekList.output));
681
+ }
682
+
233
683
  if (calendars.length === 0) {
234
684
  return { ok: true, message: "No calendars found in Calendar.app." };
235
685
  }
236
686
 
237
- const lines = calendars.map((c) => `• ${c.name}${c.writable ? "" : " (read-only)"}`);
687
+ const lines = calendars.map((c) => {
688
+ const tags = [];
689
+ if (!c.writable) tags.push("read-only");
690
+ if (c.local) tags.push("On My Mac");
691
+ else if (c.source) tags.push(c.source);
692
+ return `• ${c.name}${tags.length ? ` (${tags.join(", ")})` : ""}`;
693
+ });
238
694
  return {
239
695
  ok: true,
240
696
  message: `Calendars (${calendars.length}):\n${lines.join("\n")}\n\nPass one of these names as calendar_name when creating events.`,
@@ -318,6 +774,92 @@ export function calendarAdd(args = {}) {
318
774
  const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
319
775
  if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
320
776
 
777
+ // Non-recurring creates MUST go through EventKit. Mini 8be6cd5: EventKit
778
+ // add failed (likely writeOnly cannot list calendars) and we silently
779
+ // created via Calendar.app — remove then EVENTKIT_NOT_FOUND status=4 and
780
+ // Calendar.app delete ETIMEDOUT. No AppleScript fallback here.
781
+ if (!recurrence.rule) {
782
+ const sessionReply = eventKitSessionRequest({
783
+ op: "create",
784
+ calendarName,
785
+ title: title.text,
786
+ startSec: localUnixSeconds(start.parts),
787
+ endSec: localUnixSeconds(end.parts),
788
+ allDay,
789
+ location: location.text || "",
790
+ notes: notes.text || "",
791
+ alerts: alerts.minutes
792
+ });
793
+ if (sessionReply && sessionReply.ok) {
794
+ const ids = parseEventKitAddOutput(sessionReply.output);
795
+ if (ids.eventId) {
796
+ return {
797
+ ok: true,
798
+ message: writeSuccessMessage(action, "event created", {
799
+ event_id: ids.eventId,
800
+ eventkit_id: ids.eventKitId || ids.eventId,
801
+ calendar: ids.calendar || calendarName,
802
+ via: "EventKit",
803
+ title: truncate(title.text, 150),
804
+ start: args.start,
805
+ alerts: alerts.minutes.length ? alerts.minutes.join(", ") : undefined
806
+ })
807
+ };
808
+ }
809
+ }
810
+ if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead) {
811
+ return describeCalendarAddFailure(action, summary, {
812
+ ok: false,
813
+ error: sessionReply.error || "EVENTKIT_SESSION_CREATE_FAILED",
814
+ kind: "unknown"
815
+ });
816
+ }
817
+
818
+ // One-shot: return identifiers from the saved EKEvent. Do not re-query —
819
+ // writeOnly status=4 cannot eventWithIdentifier after save (Mini 2cdf44d).
820
+ let ek;
821
+ try {
822
+ ek = runAppleScript(
823
+ buildEventKitAddScript({
824
+ calendarName,
825
+ title: title.text,
826
+ start: start.parts,
827
+ end: end.parts,
828
+ allDay,
829
+ location: location.text,
830
+ notes: notes.text,
831
+ alerts: alerts.minutes
832
+ }),
833
+ { timeout: 30000, appName: "Calendar", language: "JavaScript" }
834
+ );
835
+ } catch (e) {
836
+ ek = { ok: false, error: e && e.message ? e.message : String(e), kind: "unknown" };
837
+ }
838
+ if (ek.ok) {
839
+ const ids = parseEventKitAddOutput(ek.output);
840
+ if (ids.eventId) {
841
+ return {
842
+ ok: true,
843
+ message: writeSuccessMessage(action, "event created", {
844
+ event_id: ids.eventId,
845
+ eventkit_id: ids.eventKitId || ids.eventId,
846
+ calendar: ids.calendar || calendarName,
847
+ via: "EventKit",
848
+ title: truncate(title.text, 150),
849
+ start: args.start,
850
+ alerts: alerts.minutes.length ? alerts.minutes.join(", ") : undefined
851
+ })
852
+ };
853
+ }
854
+ return describeCalendarAddFailure(
855
+ action,
856
+ summary,
857
+ { ok: false, error: `EVENTKIT_NO_ID output=${truncate(ek.output, 120)}`, kind: "unknown" }
858
+ );
859
+ }
860
+ return describeCalendarAddFailure(action, summary, ek);
861
+ }
862
+
321
863
  let script;
322
864
  try {
323
865
  script = buildAddEventScript({
@@ -351,6 +893,58 @@ export function calendarAdd(args = {}) {
351
893
  };
352
894
  }
353
895
 
896
+ export function buildEventKitEditScript({
897
+ eventId,
898
+ eventKitId = null,
899
+ updates = {},
900
+ start = null,
901
+ end = null,
902
+ alerts = [],
903
+ clearAlerts = false
904
+ }) {
905
+ const ids = eventKitLookupIds(eventId, eventKitId);
906
+ const title = updates.summary ? JSON.stringify(updates.summary) : "null";
907
+ const location = updates.location !== undefined ? JSON.stringify(updates.location) : "null";
908
+ const notes = updates.description !== undefined ? JSON.stringify(updates.description) : "null";
909
+ const startSec = start ? localUnixSeconds(start) : null;
910
+ const endSec = end ? localUnixSeconds(end) : null;
911
+ return `ObjC.import("EventKit");
912
+ ObjC.import("Foundation");
913
+ ${EVENTKIT_JXA_HELPERS}
914
+ var ids = ${JSON.stringify(ids)};
915
+ var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
916
+ if (status === 1 || status === 2) {
917
+ throw new Error("EVENTKIT_DENIED status=" + status);
918
+ }
919
+ var store = $.EKEventStore.alloc.init;
920
+ var event = findEventByIds(store, ids);
921
+ if (!event) {
922
+ throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " tried=" + ids.join(","));
923
+ }
924
+ var newTitle = ${title};
925
+ var newLocation = ${location};
926
+ var newNotes = ${notes};
927
+ if (newTitle !== null) event.title = newTitle;
928
+ if (newLocation !== null) event.location = newLocation;
929
+ if (newNotes !== null) event.notes = newNotes;
930
+ ${startSec !== null ? `event.startDate = $.NSDate.dateWithTimeIntervalSince1970(${asInteger(startSec, { field: "start" })});` : ""}
931
+ ${endSec !== null ? `event.endDate = $.NSDate.dateWithTimeIntervalSince1970(${asInteger(endSec, { field: "end" })});` : ""}
932
+ ${clearAlerts ? `try { event.alarms = $.NSMutableArray.array; } catch (e) {}` : ""}
933
+ ${alerts.length ? `var alarms = $.NSMutableArray.array;
934
+ for (var a = 0; a < ${JSON.stringify(alerts)}.length; a++) {
935
+ alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(${JSON.stringify(alerts)}[a]) * 60));
936
+ }
937
+ event.alarms = alarms;` : ""}
938
+ var err = Ref();
939
+ var ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
940
+ if (!ok) {
941
+ var desc = "unknown";
942
+ try { desc = String(err[0]); } catch (e) {}
943
+ throw new Error("EVENTKIT_SAVE_FAILED: " + desc);
944
+ }
945
+ jsString(event.eventIdentifier) || jsString(event.calendarItemExternalIdentifier) || "1";`;
946
+ }
947
+
354
948
  export function buildEditEventScript({ eventId, updates, start, end, rule, alerts, clearAlerts }) {
355
949
  const lines = [];
356
950
  for (const [prop, value] of Object.entries(updates)) {
@@ -384,6 +978,13 @@ export function calendarEdit(args = {}) {
384
978
  return { ok: false, message: `${action} refused: event_id is required (the "Event ID" from calendar_date or calendar_add). This tool will not guess which event you meant.` };
385
979
  }
386
980
 
981
+ const eventKitId = args.eventkit_id === undefined || args.eventkit_id === null || args.eventkit_id === ""
982
+ ? null
983
+ : validateEventKitId(args.eventkit_id);
984
+ if (args.eventkit_id && !eventKitId) {
985
+ return { ok: false, message: `${action} refused: eventkit_id is not a valid EventKit eventIdentifier.` };
986
+ }
987
+
387
988
  const updates = {};
388
989
  const changed = [];
389
990
 
@@ -438,6 +1039,66 @@ export function calendarEdit(args = {}) {
438
1039
  const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
439
1040
  if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
440
1041
 
1042
+ const preferEventKit = Boolean(eventKitId) || eventId.includes(":");
1043
+ if (preferEventKit && !recurrence.rule) {
1044
+ const sessionReply = eventKitSessionRequest({
1045
+ op: "update",
1046
+ ids: eventKitLookupIds(eventId, eventKitId),
1047
+ title: updates.summary,
1048
+ location: updates.location,
1049
+ notes: updates.description,
1050
+ startSec: start ? localUnixSeconds(start) : null,
1051
+ endSec: end ? localUnixSeconds(end) : null,
1052
+ alerts: alerts.minutes,
1053
+ clearAlerts
1054
+ });
1055
+ if (sessionReply && sessionReply.ok) {
1056
+ return {
1057
+ ok: true,
1058
+ message: writeSuccessMessage(action, "event updated", {
1059
+ event_id: eventId,
1060
+ eventkit_id: eventKitId || undefined,
1061
+ via: "EventKit",
1062
+ changed: changed.join(", ")
1063
+ })
1064
+ };
1065
+ }
1066
+ if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead && eventKitId) {
1067
+ return describeEventKitWriteFailure(action, summary, {
1068
+ ok: false,
1069
+ error: sessionReply.error || "EVENTKIT_SESSION_UPDATE_FAILED",
1070
+ kind: "not_found"
1071
+ });
1072
+ }
1073
+
1074
+ const ek = runAppleScript(
1075
+ buildEventKitEditScript({
1076
+ eventId,
1077
+ eventKitId,
1078
+ updates,
1079
+ start,
1080
+ end,
1081
+ alerts: alerts.minutes,
1082
+ clearAlerts
1083
+ }),
1084
+ { timeout: 30000, appName: "Calendar", language: "JavaScript" }
1085
+ );
1086
+ if (ek.ok) {
1087
+ return {
1088
+ ok: true,
1089
+ message: writeSuccessMessage(action, "event updated", {
1090
+ event_id: eventId,
1091
+ eventkit_id: eventKitId || undefined,
1092
+ via: "EventKit",
1093
+ changed: changed.join(", ")
1094
+ })
1095
+ };
1096
+ }
1097
+ if (eventKitId) {
1098
+ return describeEventKitWriteFailure(action, summary, ek);
1099
+ }
1100
+ }
1101
+
441
1102
  let script;
442
1103
  try {
443
1104
  script = buildEditEventScript({
@@ -462,15 +1123,68 @@ export function calendarEdit(args = {}) {
462
1123
  };
463
1124
  }
464
1125
 
465
- export function buildRemoveEventScript(eventId) {
466
- return `${findEventHandler()}
467
-
468
- set theEvent to atmFindEvent(${asString(eventId)})
469
- tell application "Calendar"
470
- set removedTitle to summary of theEvent
471
- delete theEvent
1126
+ /**
1127
+ * Delete via Calendar.app `delete` (the dictionary has no `remove` or
1128
+ * `move to trash`) using the AppleScript event `id` inside the owning
1129
+ * calendar. Do not `delete (every event whose uid …)` — that specifier
1130
+ * hangs on iCloud/CalDAV and Mini's 60s spawnSync timeout was then
1131
+ * classified as TCC. A detached `delete theEvent` after atmFindEvent
1132
+ * is also wrong (-1728). Lookup errors are not swallowed: a failed
1133
+ * delete must surface, then EventKit can run.
1134
+ */
1135
+ export function buildRemoveEventScript(eventId, { calendarName = null } = {}) {
1136
+ const uidLit = asString(eventId);
1137
+ const scoped = calendarName
1138
+ ? ` set targetCals to {}
1139
+ repeat with calRef in calendars
1140
+ try
1141
+ set cal to contents of calRef
1142
+ if (name of cal) is ${asString(calendarName)} then
1143
+ set end of targetCals to cal
1144
+ exit repeat
1145
+ end if
1146
+ end try
1147
+ end repeat
1148
+ if (count of targetCals) is 0 then error "CALENDAR_NOT_FOUND"`
1149
+ : ` set targetCals to calendars`;
1150
+
1151
+ return `tell application "Calendar"
1152
+ ${scoped}
1153
+ repeat with calRef in targetCals
1154
+ set cal to contents of calRef
1155
+ set hits to {}
1156
+ try
1157
+ set hits to (every event of cal whose uid is ${uidLit})
1158
+ end try
1159
+ if (count of hits) > 0 then
1160
+ set theEvent to item 1 of hits
1161
+ set removedTitle to summary of theEvent
1162
+ set evId to id of theEvent
1163
+ try
1164
+ with timeout of 20 seconds
1165
+ delete (event id evId of cal)
1166
+ end timeout
1167
+ on error err1 number n1
1168
+ if n1 is -1712 then error "DELETE_TIMEOUT: " & err1
1169
+ try
1170
+ with timeout of 20 seconds
1171
+ tell cal
1172
+ delete theEvent
1173
+ end tell
1174
+ end timeout
1175
+ on error err2 number n2
1176
+ if n2 is -1712 then error "DELETE_TIMEOUT: " & err2
1177
+ error "DELETE_FAILED: " & err2 number n2
1178
+ end try
1179
+ end try
1180
+ try
1181
+ reload calendars
1182
+ end try
1183
+ return removedTitle
1184
+ end if
1185
+ end repeat
472
1186
  end tell
473
- return removedTitle`;
1187
+ error "EVENT_NOT_FOUND"`;
474
1188
  }
475
1189
 
476
1190
  export function calendarRemove(args = {}) {
@@ -481,7 +1195,23 @@ export function calendarRemove(args = {}) {
481
1195
  return { ok: false, message: `${action} refused: event_id is required (the "Event ID" from calendar_date). Deletes never run on a guessed id.` };
482
1196
  }
483
1197
 
484
- const summary = `delete calendar event ${eventId}`;
1198
+ const calendarName = args.calendar_name === undefined || args.calendar_name === null || args.calendar_name === ""
1199
+ ? null
1200
+ : validateCalendarName(args.calendar_name);
1201
+ if (args.calendar_name && !calendarName) {
1202
+ return { ok: false, message: `${action} refused: calendar_name must match a calendar from calendar_list_calendars.` };
1203
+ }
1204
+
1205
+ const eventKitId = args.eventkit_id === undefined || args.eventkit_id === null || args.eventkit_id === ""
1206
+ ? null
1207
+ : validateEventKitId(args.eventkit_id);
1208
+ if (args.eventkit_id && !eventKitId) {
1209
+ return { ok: false, message: `${action} refused: eventkit_id is not a valid EventKit eventIdentifier.` };
1210
+ }
1211
+
1212
+ const summary = calendarName
1213
+ ? `delete calendar event ${eventId} from "${calendarName}"`
1214
+ : `delete calendar event ${eventId}`;
485
1215
  const plan = planWrite({
486
1216
  action,
487
1217
  summary,
@@ -491,16 +1221,65 @@ export function calendarRemove(args = {}) {
491
1221
  });
492
1222
  if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
493
1223
 
494
- const result = runAppleScript(buildRemoveEventScript(eventId), { timeout: 60000, appName: "Calendar" });
495
- if (!result.ok) return { ok: false, message: failure(action, summary, result) };
1224
+ // writeOnly cannot re-query; prefer the EventKit session that cached
1225
+ // the EKEvent from create. One-shot lookup is a full-access fallback.
1226
+ const sessionReply = eventKitSessionRequest({
1227
+ op: "remove",
1228
+ ids: eventKitLookupIds(eventId, eventKitId)
1229
+ });
1230
+ if (sessionReply && sessionReply.ok) {
1231
+ return {
1232
+ ok: true,
1233
+ message: writeSuccessMessage(action, "event deleted", {
1234
+ event_id: eventId,
1235
+ via: "EventKit",
1236
+ eventkit_id: eventKitId || undefined
1237
+ })
1238
+ };
1239
+ }
1240
+ if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead && eventKitId) {
1241
+ return describeEventKitWriteFailure(action, summary, {
1242
+ ok: false,
1243
+ error: sessionReply.error || "EVENTKIT_SESSION_REMOVE_FAILED",
1244
+ kind: "not_found"
1245
+ });
1246
+ }
496
1247
 
497
- return {
498
- ok: true,
499
- message: writeSuccessMessage(action, "event deleted", {
500
- event_id: eventId,
501
- title: truncate(result.output, 150) || undefined
502
- })
503
- };
1248
+ const eventKitResult = runAppleScript(buildEventKitRemoveScript(eventId, { eventKitId }), {
1249
+ timeout: 20000,
1250
+ appName: "Calendar",
1251
+ language: "JavaScript"
1252
+ });
1253
+ if (eventKitResult.ok) {
1254
+ return {
1255
+ ok: true,
1256
+ message: writeSuccessMessage(action, "event deleted", {
1257
+ event_id: eventId,
1258
+ via: "EventKit",
1259
+ eventkit_id: eventKitId || undefined
1260
+ })
1261
+ };
1262
+ }
1263
+
1264
+ if (eventKitId) {
1265
+ return describeEventKitWriteFailure(action, summary, eventKitResult);
1266
+ }
1267
+
1268
+ const result = runAppleScript(buildRemoveEventScript(eventId, { calendarName }), {
1269
+ timeout: 25000,
1270
+ appName: "Calendar"
1271
+ });
1272
+ if (result.ok) {
1273
+ return {
1274
+ ok: true,
1275
+ message: writeSuccessMessage(action, "event deleted", {
1276
+ event_id: eventId,
1277
+ title: truncate(result.output, 150) || undefined
1278
+ })
1279
+ };
1280
+ }
1281
+
1282
+ return describeCalendarRemoveFailure(action, summary, result, eventKitResult);
504
1283
  }
505
1284
 
506
1285
  export function buildRsvpScript({ eventId, status, attendeeEmail }) {
@@ -592,3 +1371,36 @@ export function calendarRsvp(args = {}) {
592
1371
  })
593
1372
  };
594
1373
  }
1374
+
1375
+ /**
1376
+ * First-run / upgrade probe: a live Calendar.app Apple Event that pops
1377
+ * Automation → Calendar. Listing only — no events are created, so nothing
1378
+ * is left behind.
1379
+ */
1380
+ export function buildCalendarAutomationProbeScript() {
1381
+ return `tell application "Calendar"
1382
+ get name of every calendar
1383
+ end tell
1384
+ return "OK"`;
1385
+ }
1386
+
1387
+ /**
1388
+ * Live Calendar Apple Events check. Not a dry_run. Does not create events.
1389
+ * @returns {{ ok: boolean, message: string, kind: string|null }}
1390
+ */
1391
+ export function probeCalendarAutomation() {
1392
+ const action = "calendar_automation_probe";
1393
+ const summary = "list calendars in Calendar.app (Automation / Apple Events check; no events created)";
1394
+ const result = runAppleScript(buildCalendarAutomationProbeScript(), { timeout: 30000, appName: "Calendar" });
1395
+ if (!result.ok) {
1396
+ return { ok: false, message: failure(action, summary, result), kind: result.kind };
1397
+ }
1398
+ return {
1399
+ ok: true,
1400
+ kind: null,
1401
+ message: writeSuccessMessage(
1402
+ action,
1403
+ "Calendar Automation allowed (listed calendars; no events created)"
1404
+ )
1405
+ };
1406
+ }