forge-openclaw-plugin 0.2.28 → 0.2.29

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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{board-DPFvZf-D.js → board-q8cfwaAW.js} +2 -2
  3. package/dist/assets/{board-DPFvZf-D.js.map → board-q8cfwaAW.js.map} +1 -1
  4. package/dist/assets/index-C6PCeHD_.css +1 -0
  5. package/dist/assets/index-bfHIqj0-.js +85 -0
  6. package/dist/assets/index-bfHIqj0-.js.map +1 -0
  7. package/dist/assets/{motion-Bvwc85ch.js → motion-DHfqFntt.js} +2 -2
  8. package/dist/assets/{motion-Bvwc85ch.js.map → motion-DHfqFntt.js.map} +1 -1
  9. package/dist/assets/{table-FJQTJvUR.js → table-DLweENXt.js} +2 -2
  10. package/dist/assets/{table-FJQTJvUR.js.map → table-DLweENXt.js.map} +1 -1
  11. package/dist/assets/{ui-GXFcgvSw.js → ui-BV0OYxkH.js} +2 -2
  12. package/dist/assets/{ui-GXFcgvSw.js.map → ui-BV0OYxkH.js.map} +1 -1
  13. package/dist/assets/{vendor-Cwf49UMz.js → vendor-OwcH20PM.js} +2 -2
  14. package/dist/assets/{vendor-Cwf49UMz.js.map → vendor-OwcH20PM.js.map} +1 -1
  15. package/dist/index.html +7 -7
  16. package/dist/server/server/migrations/044_macos_local_calendar_provider.sql +21 -0
  17. package/dist/server/server/src/app.js +87 -12
  18. package/dist/server/server/src/openapi.js +29 -1
  19. package/dist/server/server/src/repositories/calendar.js +144 -12
  20. package/dist/server/server/src/repositories/tasks.js +36 -17
  21. package/dist/server/server/src/services/calendar-runtime.js +613 -32
  22. package/dist/server/server/src/services/macos-calendar-helper.js +748 -0
  23. package/dist/server/server/src/types.js +46 -2
  24. package/dist/server/src/lib/api-error.js +2 -0
  25. package/dist/server/src/lib/api.js +39 -2
  26. package/dist/server/src/lib/calendar-name-deduper.js +2 -0
  27. package/openclaw.plugin.json +1 -1
  28. package/package.json +1 -1
  29. package/server/migrations/044_macos_local_calendar_provider.sql +21 -0
  30. package/skills/forge-openclaw/SKILL.md +21 -5
  31. package/skills/forge-openclaw/entity_conversation_playbooks.md +88 -5
  32. package/dist/assets/index-Auw3JrdE.css +0 -1
  33. package/dist/assets/index-D1H7myQH.js +0 -85
  34. package/dist/assets/index-D1H7myQH.js.map +0 -1
@@ -0,0 +1,748 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { execFile as execFileCallback } from "node:child_process";
3
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { resolveDataDir } from "../db.js";
8
+ const execFile = promisify(execFileCallback);
9
+ const HELPER_SOURCE = String.raw `
10
+ import AppKit
11
+ import EventKit
12
+ import Foundation
13
+
14
+ enum HelperError: Error {
15
+ case invalidRequest(String)
16
+ case unavailable(String)
17
+ }
18
+
19
+ func emit(_ payload: [String: Any]) {
20
+ let data = try! JSONSerialization.data(withJSONObject: payload, options: [])
21
+ FileHandle.standardOutput.write(data)
22
+ if let responsePath = responseFilePath() {
23
+ try? data.write(to: URL(fileURLWithPath: responsePath), options: .atomic)
24
+ }
25
+ }
26
+
27
+ func responseFilePath() -> String? {
28
+ let args = CommandLine.arguments
29
+ guard let responseIndex = args.firstIndex(of: "--response-file"), responseIndex + 1 < args.count else {
30
+ return nil
31
+ }
32
+ return args[responseIndex + 1]
33
+ }
34
+
35
+ func readRequest() throws -> [String: Any] {
36
+ let args = CommandLine.arguments
37
+ guard let requestIndex = args.firstIndex(of: "--request-base64"), requestIndex + 1 < args.count else {
38
+ throw HelperError.invalidRequest("Missing --request-base64 argument.")
39
+ }
40
+ let encoded = args[requestIndex + 1]
41
+ guard let data = Data(base64Encoded: encoded) else {
42
+ throw HelperError.invalidRequest("Invalid base64 request payload.")
43
+ }
44
+ let json = try JSONSerialization.jsonObject(with: data, options: [])
45
+ guard let payload = json as? [String: Any] else {
46
+ throw HelperError.invalidRequest("Request payload must be a JSON object.")
47
+ }
48
+ return payload
49
+ }
50
+
51
+ func authStatusText() -> String {
52
+ let status = EKEventStore.authorizationStatus(for: .event)
53
+ if #available(macOS 14.0, *) {
54
+ switch status {
55
+ case .notDetermined:
56
+ return "not_determined"
57
+ case .restricted:
58
+ return "restricted"
59
+ case .denied:
60
+ return "denied"
61
+ case .fullAccess:
62
+ return "full_access"
63
+ case .writeOnly:
64
+ return "denied"
65
+ @unknown default:
66
+ return "unavailable"
67
+ }
68
+ }
69
+
70
+ switch status {
71
+ case .notDetermined:
72
+ return "not_determined"
73
+ case .restricted:
74
+ return "restricted"
75
+ case .denied:
76
+ return "denied"
77
+ case .authorized, .fullAccess:
78
+ return "full_access"
79
+ case .writeOnly:
80
+ return "denied"
81
+ @unknown default:
82
+ return "unavailable"
83
+ }
84
+ }
85
+
86
+ func requestAccess(store: EKEventStore) throws -> [String: Any] {
87
+ if authStatusText() == "full_access" {
88
+ return [
89
+ "granted": true,
90
+ "status": "full_access"
91
+ ]
92
+ }
93
+
94
+ let semaphore = DispatchSemaphore(value: 0)
95
+ var granted = false
96
+ var capturedError: Error?
97
+
98
+ if #available(macOS 14.0, *) {
99
+ store.requestFullAccessToEvents { allowed, error in
100
+ granted = allowed
101
+ capturedError = error
102
+ semaphore.signal()
103
+ }
104
+ } else {
105
+ store.requestAccess(to: .event) { allowed, error in
106
+ granted = allowed
107
+ capturedError = error
108
+ semaphore.signal()
109
+ }
110
+ }
111
+
112
+ semaphore.wait()
113
+ if let capturedError {
114
+ throw capturedError
115
+ }
116
+
117
+ return [
118
+ "granted": granted,
119
+ "status": authStatusText()
120
+ ]
121
+ }
122
+
123
+ func sourceTypeText(_ sourceType: EKSourceType) -> String {
124
+ switch sourceType {
125
+ case .local:
126
+ return "local"
127
+ case .exchange:
128
+ return "exchange"
129
+ case .calDAV:
130
+ return "caldav"
131
+ case .mobileMe:
132
+ return "mobileme"
133
+ case .subscribed:
134
+ return "subscribed"
135
+ case .birthdays:
136
+ return "birthdays"
137
+ @unknown default:
138
+ return "unknown"
139
+ }
140
+ }
141
+
142
+ func sourceIdentifier(_ source: EKSource?) -> String {
143
+ source?.sourceIdentifier ?? "unknown-source"
144
+ }
145
+
146
+ func sourceTitle(_ source: EKSource?) -> String {
147
+ source?.title ?? "Unknown source"
148
+ }
149
+
150
+ func sourceTypeValue(_ source: EKSource?) -> String {
151
+ guard let source else {
152
+ return "unknown"
153
+ }
154
+ return sourceTypeText(source.sourceType)
155
+ }
156
+
157
+ func calendarDescription(_ calendar: EKCalendar) -> String {
158
+ ""
159
+ }
160
+
161
+ func calendarTimezoneIdentifier() -> String {
162
+ TimeZone.current.identifier
163
+ }
164
+
165
+ func calendarTypeText(_ calendarType: EKCalendarType) -> String {
166
+ switch calendarType {
167
+ case .local:
168
+ return "local"
169
+ case .calDAV:
170
+ return "caldav"
171
+ case .exchange:
172
+ return "exchange"
173
+ case .subscription:
174
+ return "subscription"
175
+ case .birthday:
176
+ return "birthday"
177
+ @unknown default:
178
+ return "unknown"
179
+ }
180
+ }
181
+
182
+ func colorHex(_ color: CGColor?) -> String {
183
+ guard let color else {
184
+ return "#7dd3fc"
185
+ }
186
+ let nsColor = NSColor(cgColor: color)?.usingColorSpace(.sRGB) ?? NSColor.systemBlue
187
+ let red = Int(round(nsColor.redComponent * 255.0))
188
+ let green = Int(round(nsColor.greenComponent * 255.0))
189
+ let blue = Int(round(nsColor.blueComponent * 255.0))
190
+ return String(format: "#%02x%02x%02x", red, green, blue)
191
+ }
192
+
193
+ func availabilityText(_ availability: EKEventAvailability) -> String {
194
+ switch availability {
195
+ case .free:
196
+ return "free"
197
+ default:
198
+ return "busy"
199
+ }
200
+ }
201
+
202
+ func isoString(_ date: Date?) -> String? {
203
+ guard let date else {
204
+ return nil
205
+ }
206
+ let formatter = ISO8601DateFormatter()
207
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
208
+ return formatter.string(from: date)
209
+ }
210
+
211
+ func parseIsoDate(_ value: String) -> Date? {
212
+ let fractionalFormatter = ISO8601DateFormatter()
213
+ fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
214
+ if let parsed = fractionalFormatter.date(from: value) {
215
+ return parsed
216
+ }
217
+ let standardFormatter = ISO8601DateFormatter()
218
+ standardFormatter.formatOptions = [.withInternetDateTime]
219
+ return standardFormatter.date(from: value)
220
+ }
221
+
222
+ func discover(store: EKEventStore) throws -> [String: Any] {
223
+ guard authStatusText() == "full_access" else {
224
+ throw HelperError.unavailable("Forge needs Calendar full access before it can read calendars already configured on this Mac.")
225
+ }
226
+
227
+ let calendars = store.calendars(for: .event)
228
+ let grouped = Dictionary(grouping: calendars, by: { sourceIdentifier($0.source) })
229
+ let sources = grouped.keys.sorted().compactMap { sourceId -> [String: Any]? in
230
+ guard let calendarsForSource = grouped[sourceId], let first = calendarsForSource.first else {
231
+ return nil
232
+ }
233
+ let source = first.source
234
+ let mappedCalendars = calendarsForSource
235
+ .sorted { lhs, rhs in
236
+ if lhs.title == rhs.title {
237
+ return lhs.calendarIdentifier < rhs.calendarIdentifier
238
+ }
239
+ return lhs.title.localizedCaseInsensitiveCompare(rhs.title) == .orderedAscending
240
+ }
241
+ .map { calendar in
242
+ [
243
+ "sourceId": sourceIdentifier(source),
244
+ "sourceTitle": sourceTitle(source),
245
+ "sourceType": sourceTypeValue(source),
246
+ "calendarId": calendar.calendarIdentifier,
247
+ "title": calendar.title,
248
+ "description": calendarDescription(calendar),
249
+ "color": colorHex(calendar.cgColor),
250
+ "timezone": calendarTimezoneIdentifier(),
251
+ "calendarType": calendarTypeText(calendar.type),
252
+ "isPrimary": calendar.allowsContentModifications && calendar.title.localizedCaseInsensitiveCompare("calendar") == .orderedSame,
253
+ "canWrite": calendar.allowsContentModifications
254
+ ] as [String : Any]
255
+ }
256
+ return [
257
+ "sourceId": sourceIdentifier(source),
258
+ "sourceTitle": sourceTitle(source),
259
+ "sourceType": sourceTypeValue(source),
260
+ "accountLabel": sourceTitle(source),
261
+ "calendars": mappedCalendars
262
+ ]
263
+ }
264
+
265
+ return [
266
+ "status": authStatusText(),
267
+ "sources": sources
268
+ ]
269
+ }
270
+
271
+ func eventPayload(_ event: EKEvent) -> [String: Any] {
272
+ [
273
+ "eventId": event.eventIdentifier ?? "",
274
+ "externalId": event.calendarItemExternalIdentifier ?? NSNull(),
275
+ "calendarId": event.calendar.calendarIdentifier,
276
+ "title": event.title ?? "(untitled event)",
277
+ "startAt": isoString(event.startDate) ?? "",
278
+ "endAt": isoString(event.endDate) ?? "",
279
+ "allDay": event.isAllDay,
280
+ "availability": availabilityText(event.availability),
281
+ "location": event.location ?? "",
282
+ "notes": event.notes ?? "",
283
+ "occurrenceDate": isoString(event.occurrenceDate) ?? NSNull(),
284
+ "lastModifiedAt": isoString(event.lastModifiedDate) ?? NSNull()
285
+ ]
286
+ }
287
+
288
+ func calendarForIdentifier(_ store: EKEventStore, _ identifier: String) -> EKCalendar? {
289
+ store.calendar(withIdentifier: identifier)
290
+ }
291
+
292
+ func listEvents(store: EKEventStore, payload: [String: Any]) throws -> [String: Any] {
293
+ guard authStatusText() == "full_access" else {
294
+ throw HelperError.unavailable("Forge needs Calendar full access before it can read local events.")
295
+ }
296
+ guard let calendarIds = payload["calendarIds"] as? [String], !calendarIds.isEmpty else {
297
+ throw HelperError.invalidRequest("calendarIds are required.")
298
+ }
299
+ guard let startRaw = payload["start"] as? String, let start = parseIsoDate(startRaw) else {
300
+ throw HelperError.invalidRequest("A valid start ISO timestamp is required.")
301
+ }
302
+ guard let endRaw = payload["end"] as? String, let end = parseIsoDate(endRaw) else {
303
+ throw HelperError.invalidRequest("A valid end ISO timestamp is required.")
304
+ }
305
+
306
+ let calendars = calendarIds.compactMap { calendarForIdentifier(store, $0) }
307
+ let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars)
308
+ let events = store.events(matching: predicate)
309
+ .sorted { lhs, rhs in
310
+ if lhs.startDate == rhs.startDate {
311
+ return (lhs.title ?? "") < (rhs.title ?? "")
312
+ }
313
+ return lhs.startDate < rhs.startDate
314
+ }
315
+ .map(eventPayload)
316
+
317
+ return ["events": events]
318
+ }
319
+
320
+ func ensureForgeCalendar(store: EKEventStore, payload: [String: Any]) throws -> [String: Any] {
321
+ guard authStatusText() == "full_access" else {
322
+ throw HelperError.unavailable("Forge needs Calendar full access before it can create or choose a local Forge calendar.")
323
+ }
324
+ guard let sourceId = payload["sourceId"] as? String, !sourceId.isEmpty else {
325
+ throw HelperError.invalidRequest("sourceId is required.")
326
+ }
327
+ let calendars = store.calendars(for: .event).filter {
328
+ sourceIdentifier($0.source) == sourceId
329
+ }
330
+ guard let source = calendars.first?.source else {
331
+ throw HelperError.invalidRequest("Unknown macOS calendar source.")
332
+ }
333
+ if let existing = calendars.first(where: { $0.title.localizedCaseInsensitiveCompare("Forge") == .orderedSame && $0.allowsContentModifications }) {
334
+ return ["calendar": [
335
+ "sourceId": source.sourceIdentifier,
336
+ "sourceTitle": source.title,
337
+ "sourceType": sourceTypeText(source.sourceType),
338
+ "calendarId": existing.calendarIdentifier,
339
+ "title": existing.title,
340
+ "description": calendarDescription(existing),
341
+ "color": colorHex(existing.cgColor),
342
+ "timezone": calendarTimezoneIdentifier(),
343
+ "calendarType": calendarTypeText(existing.type),
344
+ "isPrimary": false,
345
+ "canWrite": existing.allowsContentModifications
346
+ ]]
347
+ }
348
+
349
+ let newCalendar = EKCalendar(for: .event, eventStore: store)
350
+ newCalendar.source = source
351
+ newCalendar.title = "Forge"
352
+ newCalendar.cgColor = NSColor(calibratedRed: 0.49, green: 0.83, blue: 0.99, alpha: 1.0).cgColor
353
+ try store.saveCalendar(newCalendar, commit: true)
354
+
355
+ return ["calendar": [
356
+ "sourceId": source.sourceIdentifier,
357
+ "sourceTitle": source.title,
358
+ "sourceType": sourceTypeText(source.sourceType),
359
+ "calendarId": newCalendar.calendarIdentifier,
360
+ "title": newCalendar.title,
361
+ "description": calendarDescription(newCalendar),
362
+ "color": colorHex(newCalendar.cgColor),
363
+ "timezone": calendarTimezoneIdentifier(),
364
+ "calendarType": calendarTypeText(newCalendar.type),
365
+ "isPrimary": false,
366
+ "canWrite": newCalendar.allowsContentModifications
367
+ ]]
368
+ }
369
+
370
+ func upsertEvent(store: EKEventStore, payload: [String: Any]) throws -> [String: Any] {
371
+ guard authStatusText() == "full_access" else {
372
+ throw HelperError.unavailable("Forge needs Calendar full access before it can write local events.")
373
+ }
374
+ guard let calendarId = payload["calendarId"] as? String, let calendar = store.calendar(withIdentifier: calendarId) else {
375
+ throw HelperError.invalidRequest("calendarId is required.")
376
+ }
377
+ if !calendar.allowsContentModifications {
378
+ throw HelperError.unavailable("That local calendar is read-only.")
379
+ }
380
+ guard let title = payload["title"] as? String, !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
381
+ throw HelperError.invalidRequest("title is required.")
382
+ }
383
+ guard let startRaw = payload["startAt"] as? String, let start = parseIsoDate(startRaw) else {
384
+ throw HelperError.invalidRequest("A valid startAt ISO timestamp is required.")
385
+ }
386
+ guard let endRaw = payload["endAt"] as? String, let end = parseIsoDate(endRaw) else {
387
+ throw HelperError.invalidRequest("A valid endAt ISO timestamp is required.")
388
+ }
389
+
390
+ let eventId = payload["eventId"] as? String
391
+ let event = eventId.flatMap { store.event(withIdentifier: $0) } ?? EKEvent(eventStore: store)
392
+ event.calendar = calendar
393
+ event.title = title
394
+ event.startDate = start
395
+ event.endDate = end
396
+ event.notes = payload["notes"] as? String
397
+ event.location = payload["location"] as? String
398
+ event.isAllDay = (payload["allDay"] as? Bool) ?? false
399
+ try store.save(event, span: .thisEvent, commit: true)
400
+ return ["event": eventPayload(event)]
401
+ }
402
+
403
+ func deleteEvent(store: EKEventStore, payload: [String: Any]) throws -> [String: Any] {
404
+ guard authStatusText() == "full_access" else {
405
+ throw HelperError.unavailable("Forge needs Calendar full access before it can delete local events.")
406
+ }
407
+ guard let eventId = payload["eventId"] as? String, !eventId.isEmpty else {
408
+ throw HelperError.invalidRequest("eventId is required.")
409
+ }
410
+ guard let event = store.event(withIdentifier: eventId) else {
411
+ return ["deleted": true]
412
+ }
413
+ try store.remove(event, span: .thisEvent, commit: true)
414
+ return ["deleted": true]
415
+ }
416
+
417
+ do {
418
+ let payload = try readRequest()
419
+ let store = EKEventStore()
420
+ let command = payload["command"] as? String ?? ""
421
+ let result: [String: Any]
422
+ switch command {
423
+ case "auth_status":
424
+ result = ["status": authStatusText()]
425
+ case "request_access":
426
+ result = try requestAccess(store: store)
427
+ case "discover":
428
+ result = try discover(store: store)
429
+ case "list_events":
430
+ result = try listEvents(store: store, payload: payload)
431
+ case "ensure_forge_calendar":
432
+ result = try ensureForgeCalendar(store: store, payload: payload)
433
+ case "upsert_event":
434
+ result = try upsertEvent(store: store, payload: payload)
435
+ case "delete_event":
436
+ result = try deleteEvent(store: store, payload: payload)
437
+ default:
438
+ throw HelperError.invalidRequest("Unknown command: \(command)")
439
+ }
440
+ var response = result
441
+ response["ok"] = true
442
+ emit(response)
443
+ } catch {
444
+ emit([
445
+ "ok": false,
446
+ "error": String(describing: error)
447
+ ])
448
+ }
449
+ `;
450
+ const HELPER_INFO_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
451
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
452
+ <plist version="1.0">
453
+ <dict>
454
+ <key>CFBundleDevelopmentRegion</key>
455
+ <string>en</string>
456
+ <key>CFBundleDisplayName</key>
457
+ <string>Forge macOS Calendar Helper</string>
458
+ <key>CFBundleExecutable</key>
459
+ <string>ForgeMacOSCalendarHelper</string>
460
+ <key>CFBundleIdentifier</key>
461
+ <string>ai.openclaw.forge.macos-calendar-helper</string>
462
+ <key>CFBundleInfoDictionaryVersion</key>
463
+ <string>6.0</string>
464
+ <key>CFBundleName</key>
465
+ <string>ForgeMacOSCalendarHelper</string>
466
+ <key>CFBundlePackageType</key>
467
+ <string>APPL</string>
468
+ <key>CFBundleShortVersionString</key>
469
+ <string>1.0</string>
470
+ <key>CFBundleVersion</key>
471
+ <string>1</string>
472
+ <key>LSUIElement</key>
473
+ <true/>
474
+ <key>NSCalendarsFullAccessUsageDescription</key>
475
+ <string>Forge uses Calendar access to read and write the calendars already configured on this Mac.</string>
476
+ <key>NSCalendarsUsageDescription</key>
477
+ <string>Forge uses Calendar access to read and write the calendars already configured on this Mac.</string>
478
+ </dict>
479
+ </plist>
480
+ `;
481
+ function helperCacheDir() {
482
+ return path.join(resolveDataDir(), ".forge-native", "macos-calendar-helper");
483
+ }
484
+ function helperSourcePath() {
485
+ return path.join(helperCacheDir(), "ForgeMacOSCalendarHelper.swift");
486
+ }
487
+ function helperAppPath() {
488
+ return path.join(helperCacheDir(), "ForgeMacOSCalendarHelper.app");
489
+ }
490
+ function helperContentsPath() {
491
+ return path.join(helperAppPath(), "Contents");
492
+ }
493
+ function helperMacOSPath() {
494
+ return path.join(helperContentsPath(), "MacOS");
495
+ }
496
+ function helperInfoPlistPath() {
497
+ return path.join(helperContentsPath(), "Info.plist");
498
+ }
499
+ function helperBinaryPath() {
500
+ return path.join(helperMacOSPath(), "ForgeMacOSCalendarHelper");
501
+ }
502
+ async function ensureHelperCompiled() {
503
+ if (process.platform !== "darwin") {
504
+ throw new Error("Forge macOS local calendars are only available on macOS.");
505
+ }
506
+ const cacheDir = helperCacheDir();
507
+ const sourcePath = helperSourcePath();
508
+ const binaryPath = helperBinaryPath();
509
+ const infoPlistPath = helperInfoPlistPath();
510
+ const sourceHash = createHash("sha256").update(HELPER_SOURCE).digest("hex");
511
+ const plistHash = createHash("sha256").update(HELPER_INFO_PLIST).digest("hex");
512
+ await mkdir(cacheDir, { recursive: true });
513
+ await mkdir(helperMacOSPath(), { recursive: true });
514
+ let existingSource = "";
515
+ let existingPlist = "";
516
+ try {
517
+ existingSource = await readFile(sourcePath, "utf8");
518
+ }
519
+ catch {
520
+ existingSource = "";
521
+ }
522
+ try {
523
+ existingPlist = await readFile(infoPlistPath, "utf8");
524
+ }
525
+ catch {
526
+ existingPlist = "";
527
+ }
528
+ if (existingSource !== HELPER_SOURCE) {
529
+ await writeFile(sourcePath, HELPER_SOURCE, "utf8");
530
+ }
531
+ if (existingPlist !== HELPER_INFO_PLIST) {
532
+ await writeFile(infoPlistPath, HELPER_INFO_PLIST, "utf8");
533
+ }
534
+ let needsCompile = existingSource !== HELPER_SOURCE || existingPlist !== HELPER_INFO_PLIST;
535
+ if (!needsCompile) {
536
+ try {
537
+ const [sourceStats, plistStats, binaryStats] = await Promise.all([
538
+ stat(sourcePath),
539
+ stat(infoPlistPath),
540
+ stat(binaryPath)
541
+ ]);
542
+ needsCompile =
543
+ binaryStats.mtimeMs < sourceStats.mtimeMs ||
544
+ binaryStats.mtimeMs < plistStats.mtimeMs;
545
+ }
546
+ catch {
547
+ needsCompile = true;
548
+ }
549
+ }
550
+ if (!needsCompile) {
551
+ return { binaryPath, sourceHash, plistHash };
552
+ }
553
+ await execFile("xcrun", [
554
+ "swiftc",
555
+ "-O",
556
+ "-framework",
557
+ "EventKit",
558
+ "-framework",
559
+ "AppKit",
560
+ sourcePath,
561
+ "-o",
562
+ binaryPath
563
+ ]);
564
+ return { binaryPath, sourceHash, plistHash };
565
+ }
566
+ async function runHelperViaApp(payload) {
567
+ const { binaryPath } = await ensureHelperCompiled();
568
+ const appPath = helperAppPath();
569
+ const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
570
+ const responsePath = path.join(helperCacheDir(), `response-${randomUUID()}.json`);
571
+ try {
572
+ await execFile("open", [
573
+ "-W",
574
+ "-n",
575
+ appPath,
576
+ "--args",
577
+ "--request-base64",
578
+ encoded,
579
+ "--response-file",
580
+ responsePath
581
+ ], {
582
+ maxBuffer: 8 * 1024 * 1024,
583
+ env: {
584
+ ...process.env,
585
+ TMPDIR: process.env.TMPDIR ?? os.tmpdir()
586
+ }
587
+ });
588
+ const raw = await readFile(responsePath, "utf8");
589
+ const parsed = JSON.parse(raw);
590
+ if (!parsed.ok) {
591
+ throw new Error(parsed.error);
592
+ }
593
+ return parsed;
594
+ }
595
+ finally {
596
+ await rm(responsePath, { force: true }).catch(() => undefined);
597
+ void binaryPath;
598
+ }
599
+ }
600
+ async function runHelper(payload) {
601
+ const mockRaw = process.env.FORGE_MACOS_LOCAL_MOCK_JSON?.trim();
602
+ if (mockRaw) {
603
+ const mock = JSON.parse(mockRaw);
604
+ const status = mock.status ?? "full_access";
605
+ switch (payload.command) {
606
+ case "auth_status":
607
+ return { status };
608
+ case "request_access":
609
+ return {
610
+ granted: mock.granted ?? status === "full_access",
611
+ status
612
+ };
613
+ case "discover":
614
+ return {
615
+ status,
616
+ sources: mock.sources ?? []
617
+ };
618
+ case "list_events": {
619
+ const calendarIds = Array.isArray(payload.calendarIds)
620
+ ? payload.calendarIds.filter((value) => typeof value === "string")
621
+ : [];
622
+ return {
623
+ events: (mock.events ?? []).filter((event) => calendarIds.includes(event.calendarId))
624
+ };
625
+ }
626
+ case "ensure_forge_calendar": {
627
+ const sourceId = typeof payload.sourceId === "string" ? payload.sourceId : "";
628
+ const forgeCalendar = mock.sources
629
+ ?.find((source) => source.sourceId === sourceId)
630
+ ?.calendars.find((calendar) => calendar.title === "Forge") ?? null;
631
+ if (!forgeCalendar) {
632
+ throw new Error("Mock macOS local source is missing a Forge calendar.");
633
+ }
634
+ return { calendar: forgeCalendar };
635
+ }
636
+ case "upsert_event": {
637
+ const eventId = typeof payload.eventId === "string" && payload.eventId.trim().length > 0
638
+ ? payload.eventId
639
+ : "mock_macos_event";
640
+ return {
641
+ event: {
642
+ eventId,
643
+ externalId: eventId,
644
+ calendarId: String(payload.calendarId ?? ""),
645
+ title: String(payload.title ?? ""),
646
+ startAt: String(payload.startAt ?? ""),
647
+ endAt: String(payload.endAt ?? ""),
648
+ allDay: Boolean(payload.allDay),
649
+ availability: "busy",
650
+ location: typeof payload.location === "string" ? payload.location : "",
651
+ notes: typeof payload.notes === "string" ? payload.notes : "",
652
+ occurrenceDate: null,
653
+ lastModifiedAt: new Date().toISOString()
654
+ }
655
+ };
656
+ }
657
+ case "delete_event":
658
+ return { deleted: true };
659
+ default:
660
+ throw new Error(`Unknown mock macOS helper command ${String(payload.command)}`);
661
+ }
662
+ }
663
+ return runHelperViaApp(payload);
664
+ }
665
+ export function buildMacOSLocalCalendarUrl(sourceId, calendarId) {
666
+ return `forge-macos-local://calendar/${encodeURIComponent(sourceId)}/${encodeURIComponent(calendarId)}/`;
667
+ }
668
+ export function parseMacOSLocalCalendarUrl(urlValue) {
669
+ const url = new URL(urlValue);
670
+ if (url.protocol !== "forge-macos-local:" || url.hostname !== "calendar") {
671
+ throw new Error(`Forge could not parse macOS local calendar URL ${urlValue}.`);
672
+ }
673
+ const parts = url.pathname.split("/").filter(Boolean);
674
+ if (parts.length < 2) {
675
+ throw new Error(`Forge could not parse macOS local calendar URL ${urlValue}.`);
676
+ }
677
+ return {
678
+ sourceId: decodeURIComponent(parts[0] ?? ""),
679
+ calendarId: decodeURIComponent(parts[1] ?? "")
680
+ };
681
+ }
682
+ export async function getMacOSCalendarAuthStatus() {
683
+ if (process.platform !== "darwin") {
684
+ return { status: "unavailable" };
685
+ }
686
+ return runHelper({
687
+ command: "auth_status"
688
+ });
689
+ }
690
+ export async function requestMacOSCalendarAccess() {
691
+ if (process.platform !== "darwin") {
692
+ return {
693
+ granted: false,
694
+ status: "unavailable"
695
+ };
696
+ }
697
+ return runHelper({
698
+ command: "request_access"
699
+ });
700
+ }
701
+ export async function openMacOSCalendarPrivacySettings() {
702
+ if (process.platform !== "darwin") {
703
+ return { opened: false };
704
+ }
705
+ try {
706
+ await execFile("open", [
707
+ "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"
708
+ ]);
709
+ return { opened: true };
710
+ }
711
+ catch {
712
+ return { opened: false };
713
+ }
714
+ }
715
+ export async function discoverMacOSLocalCalendars() {
716
+ const payload = await runHelper({
717
+ command: "discover"
718
+ });
719
+ return {
720
+ status: payload.status,
721
+ requestedAt: new Date().toISOString(),
722
+ sources: payload.sources
723
+ };
724
+ }
725
+ export async function listMacOSLocalEvents(input) {
726
+ return runHelper({
727
+ command: "list_events",
728
+ ...input
729
+ });
730
+ }
731
+ export async function ensureMacOSLocalForgeCalendar(sourceId) {
732
+ return runHelper({
733
+ command: "ensure_forge_calendar",
734
+ sourceId
735
+ });
736
+ }
737
+ export async function upsertMacOSLocalEvent(input) {
738
+ return runHelper({
739
+ command: "upsert_event",
740
+ ...input
741
+ });
742
+ }
743
+ export async function deleteMacOSLocalEvent(eventId) {
744
+ return runHelper({
745
+ command: "delete_event",
746
+ eventId
747
+ });
748
+ }