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
@@ -2,13 +2,21 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
3
3
  import ical from "node-ical";
4
4
  import { logForgeDebug } from "../debug.js";
5
+ import { buildMacOSLocalCalendarUrl, deleteMacOSLocalEvent, discoverMacOSLocalCalendars, ensureMacOSLocalForgeCalendar, getMacOSCalendarAuthStatus, openMacOSCalendarPrivacySettings, parseMacOSLocalCalendarUrl, requestMacOSCalendarAccess, upsertMacOSLocalEvent, listMacOSLocalEvents } from "./macos-calendar-helper.js";
5
6
  import { getGoogleCalendarOauthCallbackPath, isGoogleCalendarOriginAllowed, isGoogleCalendarLoopbackOrigin, resolveGoogleCalendarOauthPrivateConfig } from "./google-calendar-oauth-config.js";
6
7
  import { createDAVClient, DAVNamespaceShort } from "tsdav";
7
8
  import { getSettings } from "../repositories/settings.js";
8
- import { createCalendarConnectionRecord, deleteCalendarConnectionRecord, deleteEncryptedSecret, deleteExternalEventsForConnection, detachConnectionFromForgeEvents, getCalendarById, getCalendarConnectionById, getCalendarEventStorageRecord, getCalendarOverview, listCalendarConnections, listCalendarEventSources, listCalendars, listTaskTimeboxes, markCalendarEventSourcesSyncState, readEncryptedSecret, registerCalendarEventSourceProjection, recordCalendarActivity, storeEncryptedSecret, updateCalendarConnectionRecord, updateTaskTimebox, upsertCalendarEventRecord, upsertCalendarRecord } from "../repositories/calendar.js";
9
+ import { createCalendarConnectionRecord, deleteCalendarConnectionRecord, deleteEncryptedSecret, deleteExternalEventsForConnection, detachConnectionFromForgeEvents, getCalendarById, getCalendarConnectionById, getCalendarEventStorageRecord, getCalendarOverview, isSupersededCalendarConnection, listCalendarConnections, listCalendarEventSources, listCalendars, listTaskTimeboxes, markCalendarEventSourcesSyncState, readEncryptedSecret, registerCalendarEventSourceProjection, recordCalendarActivity, storeEncryptedSecret, rehomeCalendarConnectionReferences, updateCalendarConnectionRecord, updateTaskTimebox, upsertCalendarEventRecord, upsertCalendarRecord } from "../repositories/calendar.js";
9
10
  function isWritableCalendarCredentials(credentials) {
10
11
  return credentials.provider !== "microsoft";
11
12
  }
13
+ function normalizeOptionalUrl(value) {
14
+ if (typeof value !== "string") {
15
+ return null;
16
+ }
17
+ const trimmed = value.trim();
18
+ return trimmed.length > 0 ? normalizeUrl(trimmed) : null;
19
+ }
12
20
  const GOOGLE_CALDAV_URL = "https://apidata.googleusercontent.com/caldav/v2/";
13
21
  const GOOGLE_OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
14
22
  const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -41,6 +49,14 @@ export class CalendarConnectionConflictError extends Error {
41
49
  this.connectionId = connectionId;
42
50
  }
43
51
  }
52
+ export class CalendarConnectionOverlapError extends Error {
53
+ connectionIds;
54
+ constructor(message, connectionIds) {
55
+ super(message);
56
+ this.name = "CalendarConnectionOverlapError";
57
+ this.connectionIds = connectionIds;
58
+ }
59
+ }
44
60
  function requireSecretRecord(secrets, secretId) {
45
61
  const cipherText = readEncryptedSecret(secretId);
46
62
  if (!cipherText) {
@@ -349,6 +365,13 @@ function normalizeUrl(value) {
349
365
  function normalizeAccountIdentity(value) {
350
366
  return value.trim().toLowerCase();
351
367
  }
368
+ function normalizeCalendarKey(value) {
369
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
370
+ }
371
+ function accountIdentityKeyForMacOSSource(input) {
372
+ const normalizedTitle = normalizeAccountIdentity(input.sourceTitle);
373
+ return `${input.sourceType}:${normalizedTitle}`;
374
+ }
352
375
  function buildGoogleCalendarCollectionUrl(calendarId) {
353
376
  return normalizeUrl(new URL(`${encodeURIComponent(calendarId)}/events`, GOOGLE_CALDAV_URL).toString());
354
377
  }
@@ -691,6 +714,29 @@ function mapMicrosoftEventToSyncInput(calendarId, event) {
691
714
  };
692
715
  }
693
716
  async function createProviderClient(credentials) {
717
+ if (credentials.provider === "macos_local") {
718
+ const discovery = await discoverMacOSLocalCalendars();
719
+ const source = discovery.sources.find((entry) => entry.sourceId === credentials.sourceId);
720
+ if (!source) {
721
+ throw new Error("Forge could not find that macOS calendar source anymore. Reconnect it from Settings -> Calendar.");
722
+ }
723
+ return {
724
+ mode: "macos_local",
725
+ accountLabel: source.accountLabel,
726
+ serverUrl: "forge-macos-local://eventkit/",
727
+ principalUrl: null,
728
+ homeUrl: null,
729
+ sourceId: source.sourceId,
730
+ sourceTitle: source.sourceTitle,
731
+ sourceType: source.sourceType,
732
+ accountIdentityKey: credentials.accountIdentityKey,
733
+ calendars: source.calendars.map((calendar) => ({
734
+ ...calendar,
735
+ url: buildMacOSLocalCalendarUrl(source.sourceId, calendar.calendarId)
736
+ })),
737
+ credentials
738
+ };
739
+ }
694
740
  if (credentials.provider === "microsoft") {
695
741
  const client = createMicrosoftPublicClient({
696
742
  clientId: credentials.clientId,
@@ -815,6 +861,32 @@ async function createProviderClient(credentials) {
815
861
  };
816
862
  }
817
863
  function mapDiscoveryPayload(provider, state) {
864
+ if (state.mode === "macos_local") {
865
+ return {
866
+ provider,
867
+ accountLabel: state.accountLabel,
868
+ serverUrl: state.serverUrl,
869
+ principalUrl: null,
870
+ homeUrl: null,
871
+ calendars: state.calendars.map((calendar) => ({
872
+ url: calendar.url,
873
+ displayName: safeDisplayName(calendar.title, "Calendar"),
874
+ description: calendar.description,
875
+ color: calendar.color,
876
+ timezone: normalizeTimezone(calendar.timezone),
877
+ isPrimary: calendar.isPrimary,
878
+ canWrite: calendar.canWrite,
879
+ selectedByDefault: !isForgeName(calendar.title),
880
+ isForgeCandidate: isForgeName(calendar.title),
881
+ sourceId: calendar.sourceId,
882
+ sourceTitle: calendar.sourceTitle,
883
+ sourceType: calendar.sourceType,
884
+ calendarType: calendar.calendarType,
885
+ hostCalendarId: calendar.calendarId,
886
+ canonicalKey: `${state.accountIdentityKey}:${normalizeCalendarKey(calendar.title)}`
887
+ }))
888
+ };
889
+ }
818
890
  if (state.mode === "microsoft") {
819
891
  return {
820
892
  provider,
@@ -833,7 +905,13 @@ function mapDiscoveryPayload(provider, state) {
833
905
  isPrimary: state.primaryCalendarId === calendar.id,
834
906
  canWrite: false,
835
907
  selectedByDefault: true,
836
- isForgeCandidate: false
908
+ isForgeCandidate: false,
909
+ sourceId: null,
910
+ sourceTitle: null,
911
+ sourceType: null,
912
+ calendarType: null,
913
+ hostCalendarId: null,
914
+ canonicalKey: null
837
915
  }))
838
916
  };
839
917
  }
@@ -854,7 +932,13 @@ function mapDiscoveryPayload(provider, state) {
854
932
  isPrimary: false,
855
933
  canWrite: canWriteDavCalendar(calendar),
856
934
  selectedByDefault: !isForgeName(displayName),
857
- isForgeCandidate: isForgeName(displayName)
935
+ isForgeCandidate: isForgeName(displayName),
936
+ sourceId: null,
937
+ sourceTitle: null,
938
+ sourceType: null,
939
+ calendarType: null,
940
+ hostCalendarId: null,
941
+ canonicalKey: null
858
942
  };
859
943
  })
860
944
  };
@@ -1348,6 +1432,32 @@ function mapDavObjectToEvents(calendarUrl, object, ownership) {
1348
1432
  return events;
1349
1433
  }
1350
1434
  function mapCalendarRecord(calendar, options) {
1435
+ if ("calendarId" in calendar) {
1436
+ const forgeCalendarUrl = options.forgeCalendarUrl
1437
+ ? normalizeUrl(options.forgeCalendarUrl)
1438
+ : null;
1439
+ return {
1440
+ remoteId: normalizeUrl(calendar.url),
1441
+ title: safeDisplayName(calendar.title, "Calendar"),
1442
+ description: calendar.description,
1443
+ color: calendar.color,
1444
+ timezone: normalizeTimezone(calendar.timezone),
1445
+ isPrimary: calendar.isPrimary,
1446
+ canWrite: calendar.canWrite,
1447
+ selectedForSync: forgeCalendarUrl
1448
+ ? normalizeUrl(calendar.url) !== forgeCalendarUrl
1449
+ : true,
1450
+ forgeManaged: forgeCalendarUrl
1451
+ ? normalizeUrl(calendar.url) === forgeCalendarUrl
1452
+ : false,
1453
+ sourceId: calendar.sourceId,
1454
+ sourceTitle: calendar.sourceTitle,
1455
+ sourceType: calendar.sourceType,
1456
+ calendarType: calendar.calendarType,
1457
+ hostCalendarId: calendar.calendarId,
1458
+ canonicalKey: `${options.accountIdentityKey ?? ""}:${normalizeCalendarKey(calendar.title)}`
1459
+ };
1460
+ }
1351
1461
  if ("url" in calendar) {
1352
1462
  const forgeCalendarUrl = options.forgeCalendarUrl ? normalizeUrl(options.forgeCalendarUrl) : null;
1353
1463
  const title = safeDisplayName(calendar.displayName, "Calendar");
@@ -1361,7 +1471,13 @@ function mapCalendarRecord(calendar, options) {
1361
1471
  isPrimary: false,
1362
1472
  canWrite: canWriteDavCalendar(calendar),
1363
1473
  selectedForSync: forgeCalendarUrl ? remoteUrl !== forgeCalendarUrl : true,
1364
- forgeManaged: forgeCalendarUrl ? remoteUrl === forgeCalendarUrl : false
1474
+ forgeManaged: forgeCalendarUrl ? remoteUrl === forgeCalendarUrl : false,
1475
+ sourceId: null,
1476
+ sourceTitle: null,
1477
+ sourceType: null,
1478
+ calendarType: null,
1479
+ hostCalendarId: null,
1480
+ canonicalKey: remoteUrl
1365
1481
  };
1366
1482
  }
1367
1483
  return {
@@ -1375,11 +1491,73 @@ function mapCalendarRecord(calendar, options) {
1375
1491
  isPrimary: options.primaryCalendarId === calendar.id,
1376
1492
  canWrite: false,
1377
1493
  selectedForSync: true,
1378
- forgeManaged: false
1494
+ forgeManaged: false,
1495
+ sourceId: null,
1496
+ sourceTitle: null,
1497
+ sourceType: null,
1498
+ calendarType: null,
1499
+ hostCalendarId: null,
1500
+ canonicalKey: microsoftCalendarUrl(calendar.id)
1501
+ };
1502
+ }
1503
+ function mapMacOSLocalEventToSyncInput(calendarUrl, event, ownership) {
1504
+ return {
1505
+ calendarRemoteId: normalizeUrl(calendarUrl),
1506
+ remoteId: event.eventId,
1507
+ remoteHref: null,
1508
+ remoteEtag: null,
1509
+ ownership,
1510
+ status: "confirmed",
1511
+ title: event.title,
1512
+ description: event.notes,
1513
+ location: event.location,
1514
+ startAt: event.startAt,
1515
+ endAt: event.endAt,
1516
+ isAllDay: event.allDay,
1517
+ availability: event.availability,
1518
+ eventType: "",
1519
+ categories: [],
1520
+ rawPayload: {
1521
+ externalId: event.externalId,
1522
+ occurrenceDate: event.occurrenceDate
1523
+ },
1524
+ remoteUpdatedAt: event.lastModifiedAt,
1525
+ deletedAt: null
1379
1526
  };
1380
1527
  }
1381
1528
  async function publishTaskTimeboxes(state, forgeCalendarUrl, connectionId) {
1382
- if (state.mode !== "dav" || !forgeCalendarUrl) {
1529
+ if (!forgeCalendarUrl) {
1530
+ return;
1531
+ }
1532
+ if (state.mode === "macos_local") {
1533
+ const forgeCalendar = state.calendars.find((calendar) => normalizeUrl(calendar.url) === normalizeUrl(forgeCalendarUrl));
1534
+ if (!forgeCalendar || !forgeCalendar.canWrite) {
1535
+ return;
1536
+ }
1537
+ const horizon = {
1538
+ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
1539
+ to: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString()
1540
+ };
1541
+ const timeboxes = listTaskTimeboxes(horizon);
1542
+ for (const timebox of timeboxes) {
1543
+ const { event } = await upsertMacOSLocalEvent({
1544
+ calendarId: forgeCalendar.calendarId,
1545
+ eventId: timebox.remoteEventId,
1546
+ title: timebox.title,
1547
+ startAt: timebox.startsAt,
1548
+ endAt: timebox.endsAt,
1549
+ notes: timebox.overrideReason ?? ""
1550
+ });
1551
+ const localForgeCalendar = listCalendars(connectionId).find((entry) => normalizeUrl(entry.remoteId) === normalizeUrl(forgeCalendar.url));
1552
+ updateTaskTimebox(timebox.id, {
1553
+ connectionId,
1554
+ calendarId: localForgeCalendar?.id ?? null,
1555
+ remoteEventId: event.eventId
1556
+ });
1557
+ }
1558
+ return;
1559
+ }
1560
+ if (state.mode !== "dav") {
1383
1561
  return;
1384
1562
  }
1385
1563
  const forgeCalendar = state.calendars.find((calendar) => normalizeUrl(calendar.url) === normalizeUrl(forgeCalendarUrl));
@@ -1425,6 +1603,55 @@ async function publishTaskTimeboxes(state, forgeCalendarUrl, connectionId) {
1425
1603
  }
1426
1604
  async function syncDiscoveredState(connectionId, credentials) {
1427
1605
  const state = await createProviderClient(credentials);
1606
+ if (state.mode === "macos_local") {
1607
+ const selected = new Set(credentials.selectedCalendarUrls.map((value) => normalizeUrl(value)));
1608
+ const forgeCalendarUrl = normalizeOptionalUrl(credentials.forgeCalendarUrl);
1609
+ for (const calendar of state.calendars) {
1610
+ const normalized = normalizeUrl(calendar.url);
1611
+ upsertCalendarRecord(connectionId, {
1612
+ ...mapCalendarRecord(calendar, {
1613
+ forgeCalendarUrl,
1614
+ accountIdentityKey: state.accountIdentityKey
1615
+ }),
1616
+ selectedForSync: selected.has(normalized)
1617
+ });
1618
+ if (!selected.has(normalized) && normalized !== forgeCalendarUrl) {
1619
+ continue;
1620
+ }
1621
+ }
1622
+ const calendarIds = state.calendars
1623
+ .filter((calendar) => {
1624
+ const normalized = normalizeUrl(calendar.url);
1625
+ return (selected.has(normalized) ||
1626
+ normalized === forgeCalendarUrl);
1627
+ })
1628
+ .map((calendar) => calendar.calendarId);
1629
+ if (calendarIds.length > 0) {
1630
+ const now = new Date();
1631
+ const start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1632
+ const end = new Date(now.getTime() + 180 * 24 * 60 * 60 * 1000).toISOString();
1633
+ const { events } = await listMacOSLocalEvents({
1634
+ calendarIds,
1635
+ start,
1636
+ end
1637
+ });
1638
+ const calendarsById = new Map(state.calendars.map((calendar) => [calendar.calendarId, calendar]));
1639
+ for (const event of events) {
1640
+ const calendar = calendarsById.get(event.calendarId);
1641
+ if (!calendar) {
1642
+ continue;
1643
+ }
1644
+ const ownership = forgeCalendarUrl && normalizeUrl(calendar.url) === forgeCalendarUrl
1645
+ ? "forge"
1646
+ : "external";
1647
+ upsertCalendarEventRecord(connectionId, mapMacOSLocalEventToSyncInput(calendar.url, event, ownership));
1648
+ }
1649
+ }
1650
+ return {
1651
+ state,
1652
+ forgeCalendarUrl
1653
+ };
1654
+ }
1428
1655
  if (!isWritableCalendarCredentials(credentials)) {
1429
1656
  if (state.mode !== "microsoft") {
1430
1657
  throw new Error("Forge expected a Microsoft provider state for this calendar connection.");
@@ -1460,17 +1687,19 @@ async function syncDiscoveredState(connectionId, credentials) {
1460
1687
  throw new Error("Forge expected a DAV provider state for this writable calendar connection.");
1461
1688
  }
1462
1689
  const selected = new Set(credentials.selectedCalendarUrls.map((value) => normalizeUrl(value)));
1463
- const forgeCalendarUrl = normalizeUrl(credentials.forgeCalendarUrl);
1690
+ const forgeCalendarUrl = normalizeOptionalUrl(credentials.forgeCalendarUrl);
1464
1691
  const calendarsToSync = state.calendars.filter((calendar) => {
1465
1692
  const normalized = normalizeUrl(calendar.url);
1466
1693
  if (selected.has(normalized)) {
1467
1694
  return true;
1468
1695
  }
1469
- if (credentials.provider === "google" && normalized === forgeCalendarUrl) {
1696
+ if (credentials.provider === "google" &&
1697
+ forgeCalendarUrl &&
1698
+ normalized === forgeCalendarUrl) {
1470
1699
  logForgeDebug(`[forge-calendar-sync] skip_forge_readback connectionId=${JSON.stringify(connectionId)} provider=${JSON.stringify(credentials.provider)} calendarUrl=${JSON.stringify(normalized)}`);
1471
1700
  return false;
1472
1701
  }
1473
- return normalized === forgeCalendarUrl;
1702
+ return forgeCalendarUrl ? normalized === forgeCalendarUrl : false;
1474
1703
  });
1475
1704
  for (const calendar of state.calendars) {
1476
1705
  const normalized = normalizeUrl(calendar.url);
@@ -1483,7 +1712,9 @@ async function syncDiscoveredState(connectionId, credentials) {
1483
1712
  const start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
1484
1713
  const end = new Date(now.getTime() + 180 * 24 * 60 * 60 * 1000).toISOString();
1485
1714
  for (const calendar of calendarsToSync) {
1486
- const ownership = normalizeUrl(calendar.url) === forgeCalendarUrl ? "forge" : "external";
1715
+ const ownership = forgeCalendarUrl && normalizeUrl(calendar.url) === forgeCalendarUrl
1716
+ ? "forge"
1717
+ : "external";
1487
1718
  const calendarUrl = normalizeUrl(calendar.url);
1488
1719
  logForgeDebug(`[forge-calendar-sync] fetch_calendar_objects_start connectionId=${JSON.stringify(connectionId)} provider=${JSON.stringify(credentials.provider)} calendarUrl=${JSON.stringify(calendarUrl)} ownership=${JSON.stringify(ownership)}`);
1489
1720
  if (credentials.provider === "google") {
@@ -1529,7 +1760,7 @@ function toStoredCredentials(input, forgeCalendarUrl) {
1529
1760
  username: input.username,
1530
1761
  password: input.password,
1531
1762
  selectedCalendarUrls: input.selectedCalendarUrls.map(normalizeUrl),
1532
- forgeCalendarUrl: normalizeUrl(forgeCalendarUrl)
1763
+ forgeCalendarUrl: normalizeOptionalUrl(forgeCalendarUrl)
1533
1764
  };
1534
1765
  }
1535
1766
  return {
@@ -1538,7 +1769,7 @@ function toStoredCredentials(input, forgeCalendarUrl) {
1538
1769
  username: input.username,
1539
1770
  password: input.password,
1540
1771
  selectedCalendarUrls: input.selectedCalendarUrls.map(normalizeUrl),
1541
- forgeCalendarUrl: normalizeUrl(forgeCalendarUrl)
1772
+ forgeCalendarUrl: normalizeOptionalUrl(forgeCalendarUrl)
1542
1773
  };
1543
1774
  }
1544
1775
  function credentialsMatch(existing, incoming) {
@@ -1569,6 +1800,73 @@ function findExistingCalendarConnection(incoming, secrets) {
1569
1800
  }
1570
1801
  });
1571
1802
  }
1803
+ function readConnectionAccountIdentityKey(connection, secrets) {
1804
+ const configuredKey = typeof connection.config.accountIdentityKey === "string"
1805
+ ? connection.config.accountIdentityKey.trim()
1806
+ : "";
1807
+ if (configuredKey) {
1808
+ return normalizeAccountIdentity(configuredKey);
1809
+ }
1810
+ try {
1811
+ const existing = requireSecretRecord(secrets, connection.credentialsSecretId);
1812
+ if (existing.provider === "macos_local") {
1813
+ return normalizeAccountIdentity(existing.accountIdentityKey);
1814
+ }
1815
+ if ("username" in existing && typeof existing.username === "string") {
1816
+ return normalizeAccountIdentity(existing.username);
1817
+ }
1818
+ }
1819
+ catch {
1820
+ // Fall back to account label below.
1821
+ }
1822
+ return normalizeAccountIdentity(connection.accountLabel);
1823
+ }
1824
+ function findOverlappingConnectionsForAccount(accountIdentityKey, secrets) {
1825
+ const target = normalizeAccountIdentity(accountIdentityKey);
1826
+ return listCalendarConnections().filter((connection) => {
1827
+ if (typeof connection.config.replacedByConnectionId === "string" &&
1828
+ connection.config.replacedByConnectionId.trim().length > 0) {
1829
+ return false;
1830
+ }
1831
+ return readConnectionAccountIdentityKey(connection, secrets) === target;
1832
+ });
1833
+ }
1834
+ function findExistingForgeWriteTarget(options) {
1835
+ const excluded = new Set(options?.excludeConnectionIds ?? []);
1836
+ return listCalendarConnections().find((connection) => {
1837
+ if (excluded.has(connection.id)) {
1838
+ return false;
1839
+ }
1840
+ if (typeof connection.config.replacedByConnectionId === "string" &&
1841
+ connection.config.replacedByConnectionId.trim().length > 0) {
1842
+ return false;
1843
+ }
1844
+ return normalizeOptionalUrl(typeof connection.config.forgeCalendarUrl === "string"
1845
+ ? connection.config.forgeCalendarUrl
1846
+ : null) !== null;
1847
+ });
1848
+ }
1849
+ function supersedeCalendarConnections(connectionIds, replacingConnectionId) {
1850
+ for (const connectionId of connectionIds) {
1851
+ const connection = getCalendarConnectionById(connectionId);
1852
+ if (!connection) {
1853
+ continue;
1854
+ }
1855
+ updateCalendarConnectionRecord(connectionId, {
1856
+ status: "needs_attention",
1857
+ config: {
1858
+ ...connection.config,
1859
+ replacedByConnectionId: replacingConnectionId,
1860
+ replacementTransport: "macos_local"
1861
+ }
1862
+ });
1863
+ }
1864
+ }
1865
+ function requireActiveConnection(connectionId) {
1866
+ if (isSupersededCalendarConnection(connectionId)) {
1867
+ throw new Error("This calendar connection has been replaced by a newer canonical connection and can no longer be synced directly.");
1868
+ }
1869
+ }
1572
1870
  function toDiscoveryCredentials(input) {
1573
1871
  if (input.provider === "microsoft" || input.provider === "google") {
1574
1872
  throw new Error(`${input.provider === "google" ? "Google Calendar" : "Exchange Online"} discovery now uses the guided OAuth sign-in flow.`);
@@ -1592,7 +1890,72 @@ export async function discoverCalendarConnection(input) {
1592
1890
  const state = await createProviderClient(toDiscoveryCredentials(input));
1593
1891
  return mapDiscoveryPayload(input.provider, state);
1594
1892
  }
1893
+ export async function getMacOSLocalCalendarAccessStatus() {
1894
+ return getMacOSCalendarAuthStatus();
1895
+ }
1896
+ export async function requestMacOSLocalCalendarAccess() {
1897
+ const result = await requestMacOSCalendarAccess();
1898
+ if (result.granted) {
1899
+ return result;
1900
+ }
1901
+ if (result.status === "not_determined") {
1902
+ const settings = await openMacOSCalendarPrivacySettings();
1903
+ return {
1904
+ ...result,
1905
+ promptSuppressed: true,
1906
+ openedSystemSettings: settings.opened,
1907
+ message: settings.opened
1908
+ ? "macOS did not present the Calendar permission prompt from the Forge background service. Forge opened System Settings > Privacy & Security > Calendars so you can allow access there, then return here and click Check access."
1909
+ : "macOS did not present the Calendar permission prompt from the Forge background service. Open System Settings > Privacy & Security > Calendars, allow Forge, then return here and click Check access."
1910
+ };
1911
+ }
1912
+ if (result.status === "denied" || result.status === "restricted") {
1913
+ return {
1914
+ ...result,
1915
+ message: "Calendar access is blocked for Forge. Open System Settings > Privacy & Security > Calendars, allow Forge, then return here and click Check access."
1916
+ };
1917
+ }
1918
+ return result;
1919
+ }
1920
+ export async function discoverMacOSLocalCalendarSources() {
1921
+ const discovery = await discoverMacOSLocalCalendars();
1922
+ return {
1923
+ status: discovery.status,
1924
+ requestedAt: discovery.requestedAt,
1925
+ sources: discovery.sources.map((source) => ({
1926
+ sourceId: source.sourceId,
1927
+ sourceTitle: source.sourceTitle,
1928
+ sourceType: source.sourceType,
1929
+ accountLabel: source.accountLabel,
1930
+ accountIdentityKey: accountIdentityKeyForMacOSSource({
1931
+ sourceTitle: source.sourceTitle,
1932
+ sourceType: source.sourceType
1933
+ }),
1934
+ calendars: source.calendars.map((calendar) => ({
1935
+ url: buildMacOSLocalCalendarUrl(source.sourceId, calendar.calendarId),
1936
+ displayName: calendar.title,
1937
+ description: calendar.description,
1938
+ color: calendar.color,
1939
+ timezone: normalizeTimezone(calendar.timezone),
1940
+ isPrimary: calendar.isPrimary,
1941
+ canWrite: calendar.canWrite,
1942
+ selectedByDefault: !isForgeName(calendar.title),
1943
+ isForgeCandidate: isForgeName(calendar.title),
1944
+ sourceId: source.sourceId,
1945
+ sourceTitle: source.sourceTitle,
1946
+ sourceType: source.sourceType,
1947
+ calendarType: calendar.calendarType,
1948
+ hostCalendarId: calendar.calendarId,
1949
+ canonicalKey: `${accountIdentityKeyForMacOSSource({
1950
+ sourceTitle: source.sourceTitle,
1951
+ sourceType: source.sourceType
1952
+ })}:${normalizeCalendarKey(calendar.title)}`
1953
+ }))
1954
+ }))
1955
+ };
1956
+ }
1595
1957
  export async function discoverExistingCalendarConnection(connectionId, secrets) {
1958
+ requireActiveConnection(connectionId);
1596
1959
  const connection = getCalendarConnectionById(connectionId);
1597
1960
  if (!connection) {
1598
1961
  throw new Error(`Unknown calendar connection ${connectionId}`);
@@ -1602,6 +1965,103 @@ export async function discoverExistingCalendarConnection(connectionId, secrets)
1602
1965
  return mapDiscoveryPayload(connection.provider, state);
1603
1966
  }
1604
1967
  export async function createCalendarConnection(input, secrets, activity = { source: "ui" }) {
1968
+ const cleanupFailedConnectionCreation = (connectionId, secretId) => {
1969
+ try {
1970
+ deleteCalendarConnectionRecord(connectionId);
1971
+ }
1972
+ catch {
1973
+ // Best-effort cleanup for partially-created connections.
1974
+ }
1975
+ try {
1976
+ deleteEncryptedSecret(secretId);
1977
+ }
1978
+ catch {
1979
+ // Best-effort cleanup for partially-created credentials.
1980
+ }
1981
+ };
1982
+ if (input.provider === "macos_local") {
1983
+ const discovery = await discoverMacOSLocalCalendars();
1984
+ if (discovery.status !== "full_access") {
1985
+ throw new Error("Forge needs Calendar full access before it can connect the calendars already configured on this Mac.");
1986
+ }
1987
+ const source = discovery.sources.find((entry) => entry.sourceId === input.sourceId);
1988
+ if (!source) {
1989
+ throw new Error("Forge could not find that macOS calendar source anymore. Discover again and retry.");
1990
+ }
1991
+ const accountIdentityKey = accountIdentityKeyForMacOSSource({
1992
+ sourceTitle: source.sourceTitle,
1993
+ sourceType: source.sourceType
1994
+ });
1995
+ const overlaps = findOverlappingConnectionsForAccount(accountIdentityKey, secrets);
1996
+ const replaceIds = new Set(input.replaceConnectionIds ?? []);
1997
+ const unresolvedOverlaps = overlaps.filter((connection) => !replaceIds.has(connection.id));
1998
+ if (unresolvedOverlaps.length > 0) {
1999
+ throw new CalendarConnectionOverlapError(`Forge already syncs ${source.accountLabel || source.sourceTitle} through another calendar connection. Replace the older connection instead of keeping two copies of the same calendar account.`, unresolvedOverlaps.map((connection) => connection.id));
2000
+ }
2001
+ const discoveredCalendars = source.calendars.map((calendar) => ({
2002
+ ...calendar,
2003
+ url: buildMacOSLocalCalendarUrl(source.sourceId, calendar.calendarId)
2004
+ }));
2005
+ const sharedWriteTarget = findExistingForgeWriteTarget({
2006
+ excludeConnectionIds: Array.from(replaceIds)
2007
+ });
2008
+ let forgeCalendarUrl = input.forgeCalendarUrl?.trim() ||
2009
+ (!sharedWriteTarget
2010
+ ? discoveredCalendars.find((calendar) => isForgeName(calendar.title))?.url
2011
+ : null) ||
2012
+ null;
2013
+ if (!forgeCalendarUrl && input.createForgeCalendar && !sharedWriteTarget) {
2014
+ const created = await ensureMacOSLocalForgeCalendar(source.sourceId);
2015
+ forgeCalendarUrl = buildMacOSLocalCalendarUrl(created.calendar.sourceId, created.calendar.calendarId);
2016
+ }
2017
+ if (!forgeCalendarUrl && !sharedWriteTarget) {
2018
+ throw new Error("Select the calendar Forge should write into, ask Forge to create one, or keep using the existing shared Forge write target.");
2019
+ }
2020
+ const secretId = `calendar_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
2021
+ const storedCredentials = {
2022
+ provider: "macos_local",
2023
+ sourceId: source.sourceId,
2024
+ sourceTitle: source.sourceTitle,
2025
+ sourceType: source.sourceType,
2026
+ accountIdentityKey,
2027
+ selectedCalendarUrls: input.selectedCalendarUrls.map(normalizeUrl),
2028
+ forgeCalendarUrl: normalizeOptionalUrl(forgeCalendarUrl)
2029
+ };
2030
+ storeEncryptedSecret(secretId, secrets.sealJson(storedCredentials), `${input.label} macOS local calendar credentials`);
2031
+ const connection = createCalendarConnectionRecord({
2032
+ provider: "macos_local",
2033
+ label: input.label,
2034
+ accountLabel: source.accountLabel,
2035
+ config: {
2036
+ serverUrl: "forge-macos-local://eventkit/",
2037
+ transportKind: "macos_local",
2038
+ sourceId: source.sourceId,
2039
+ sourceType: source.sourceType,
2040
+ accountIdentityKey,
2041
+ selectedCalendarCount: storedCredentials.selectedCalendarUrls.length,
2042
+ forgeCalendarUrl: storedCredentials.forgeCalendarUrl
2043
+ },
2044
+ credentialsSecretId: secretId
2045
+ });
2046
+ try {
2047
+ await syncCalendarConnection(connection.id, secrets, activity);
2048
+ }
2049
+ catch (error) {
2050
+ cleanupFailedConnectionCreation(connection.id, secretId);
2051
+ throw error;
2052
+ }
2053
+ if (replaceIds.size > 0) {
2054
+ for (const replacedConnectionId of replaceIds) {
2055
+ rehomeCalendarConnectionReferences({
2056
+ fromConnectionId: replacedConnectionId,
2057
+ toConnectionId: connection.id
2058
+ });
2059
+ }
2060
+ supersedeCalendarConnections(Array.from(replaceIds), connection.id);
2061
+ }
2062
+ recordCalendarActivity("calendar_connection_created", "calendar_connection", connection.id, `Calendar connection created: ${connection.label}`, "Forge is now mirroring the calendars already configured on this Mac through EventKit.", activity, { provider: input.provider });
2063
+ return getCalendarConnectionById(connection.id);
2064
+ }
1605
2065
  if (input.provider === "google") {
1606
2066
  pruneGoogleOauthSessions();
1607
2067
  const session = googleOauthSessions.get(input.authSessionId);
@@ -1626,21 +2086,24 @@ export async function createCalendarConnection(input, secrets, activity = { sour
1626
2086
  if (state.mode !== "dav") {
1627
2087
  throw new Error("Forge expected a writable DAV provider state for this Google Calendar connection.");
1628
2088
  }
2089
+ const sharedWriteTarget = findExistingForgeWriteTarget();
1629
2090
  let forgeCalendarUrl = input.forgeCalendarUrl?.trim() ||
1630
- session.discovery.calendars.find((calendar) => calendar.isForgeCandidate)?.url ||
2091
+ (!sharedWriteTarget
2092
+ ? session.discovery.calendars.find((calendar) => calendar.isForgeCandidate)?.url
2093
+ : null) ||
1631
2094
  null;
1632
- if (!forgeCalendarUrl && input.createForgeCalendar) {
2095
+ if (!forgeCalendarUrl && input.createForgeCalendar && !sharedWriteTarget) {
1633
2096
  const created = await ensureForgeCalendar(state);
1634
2097
  forgeCalendarUrl = created.forgeCalendarUrl;
1635
2098
  }
1636
- if (!forgeCalendarUrl) {
1637
- throw new Error("Select the calendar Forge should write into, or create a new calendar named Forge.");
2099
+ if (!forgeCalendarUrl && !sharedWriteTarget) {
2100
+ throw new Error("Select the calendar Forge should write into, ask Forge to create one, or keep using the existing shared Forge write target.");
1638
2101
  }
1639
2102
  const secretId = `calendar_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1640
2103
  const storedCredentials = {
1641
2104
  ...session.credentials,
1642
2105
  selectedCalendarUrls: input.selectedCalendarUrls.map(normalizeUrl),
1643
- forgeCalendarUrl: normalizeUrl(forgeCalendarUrl)
2106
+ forgeCalendarUrl: normalizeOptionalUrl(forgeCalendarUrl)
1644
2107
  };
1645
2108
  // The app credentials belong to Forge itself. Only user-specific OAuth tokens
1646
2109
  // are stored per calendar connection.
@@ -1652,11 +2115,17 @@ export async function createCalendarConnection(input, secrets, activity = { sour
1652
2115
  config: {
1653
2116
  serverUrl: session.discovery.serverUrl,
1654
2117
  selectedCalendarCount: storedCredentials.selectedCalendarUrls.length,
1655
- forgeCalendarUrl: normalizeUrl(storedCredentials.forgeCalendarUrl)
2118
+ forgeCalendarUrl: normalizeOptionalUrl(storedCredentials.forgeCalendarUrl)
1656
2119
  },
1657
2120
  credentialsSecretId: secretId
1658
2121
  });
1659
- await syncCalendarConnection(connection.id, secrets, activity);
2122
+ try {
2123
+ await syncCalendarConnection(connection.id, secrets, activity);
2124
+ }
2125
+ catch (error) {
2126
+ cleanupFailedConnectionCreation(connection.id, secretId);
2127
+ throw error;
2128
+ }
1660
2129
  session.status = "consumed";
1661
2130
  recordCalendarActivity("calendar_connection_created", "calendar_connection", connection.id, `Calendar connection created: ${connection.label}`, "Google Calendar is now connected to Forge through the Authorization Code + PKCE flow.", activity, { provider: input.provider });
1662
2131
  return getCalendarConnectionById(connection.id);
@@ -1699,7 +2168,13 @@ export async function createCalendarConnection(input, secrets, activity = { sour
1699
2168
  },
1700
2169
  credentialsSecretId: secretId
1701
2170
  });
1702
- await syncCalendarConnection(connection.id, secrets, activity);
2171
+ try {
2172
+ await syncCalendarConnection(connection.id, secrets, activity);
2173
+ }
2174
+ catch (error) {
2175
+ cleanupFailedConnectionCreation(connection.id, secretId);
2176
+ throw error;
2177
+ }
1703
2178
  session.status = "consumed";
1704
2179
  recordCalendarActivity("calendar_connection_created", "calendar_connection", connection.id, `Calendar connection created: ${connection.label}`, "Exchange Online is now connected to Forge through Microsoft sign-in in read-only mode.", activity, { provider: input.provider });
1705
2180
  return getCalendarConnectionById(connection.id);
@@ -1714,17 +2189,20 @@ export async function createCalendarConnection(input, secrets, activity = { sour
1714
2189
  throw new Error("Forge expected a writable DAV provider state for this calendar connection.");
1715
2190
  }
1716
2191
  const discovery = mapDiscoveryPayload(input.provider, state);
2192
+ const sharedWriteTarget = findExistingForgeWriteTarget();
1717
2193
  let forgeCalendarUrl = null;
1718
2194
  forgeCalendarUrl =
1719
2195
  input.forgeCalendarUrl?.trim() ||
1720
- discovery.calendars.find((calendar) => calendar.isForgeCandidate)?.url ||
2196
+ (!sharedWriteTarget
2197
+ ? discovery.calendars.find((calendar) => calendar.isForgeCandidate)?.url
2198
+ : null) ||
1721
2199
  null;
1722
- if (!forgeCalendarUrl && input.createForgeCalendar) {
2200
+ if (!forgeCalendarUrl && input.createForgeCalendar && !sharedWriteTarget) {
1723
2201
  const created = await ensureForgeCalendar(state);
1724
2202
  forgeCalendarUrl = created.forgeCalendarUrl;
1725
2203
  }
1726
- if (!forgeCalendarUrl) {
1727
- throw new Error("Select the calendar Forge should write into, or create a new calendar named Forge.");
2204
+ if (!forgeCalendarUrl && !sharedWriteTarget) {
2205
+ throw new Error("Select the calendar Forge should write into, ask Forge to create one, or keep using the existing shared Forge write target.");
1728
2206
  }
1729
2207
  const secretId = `calendar_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1730
2208
  const storedCredentials = toStoredCredentials(input, forgeCalendarUrl);
@@ -1736,11 +2214,17 @@ export async function createCalendarConnection(input, secrets, activity = { sour
1736
2214
  config: {
1737
2215
  serverUrl: discovery.serverUrl,
1738
2216
  selectedCalendarCount: storedCredentials.selectedCalendarUrls.length,
1739
- forgeCalendarUrl: normalizeUrl(storedCredentials.forgeCalendarUrl)
2217
+ forgeCalendarUrl: normalizeOptionalUrl(storedCredentials.forgeCalendarUrl)
1740
2218
  },
1741
2219
  credentialsSecretId: secretId
1742
2220
  });
1743
- await syncCalendarConnection(connection.id, secrets, activity);
2221
+ try {
2222
+ await syncCalendarConnection(connection.id, secrets, activity);
2223
+ }
2224
+ catch (error) {
2225
+ cleanupFailedConnectionCreation(connection.id, secretId);
2226
+ throw error;
2227
+ }
1744
2228
  recordCalendarActivity("calendar_connection_created", "calendar_connection", connection.id, `Calendar connection created: ${connection.label}`, `${input.provider === "apple" ? "Apple Calendar" : "Custom CalDAV"} is now connected to Forge.`, activity, { provider: input.provider });
1745
2229
  return getCalendarConnectionById(connection.id);
1746
2230
  }
@@ -1757,6 +2241,7 @@ export async function removeCalendarConnection(connectionId, secrets, activity =
1757
2241
  return connection;
1758
2242
  }
1759
2243
  export async function syncCalendarConnection(connectionId, secrets, activity = { source: "system" }) {
2244
+ requireActiveConnection(connectionId);
1760
2245
  const connection = getCalendarConnectionById(connectionId);
1761
2246
  if (!connection) {
1762
2247
  throw new Error(`Unknown calendar connection ${connectionId}`);
@@ -1775,7 +2260,9 @@ export async function syncCalendarConnection(connectionId, secrets, activity = {
1775
2260
  forgeCalendarId: forgeCalendar?.id ?? null,
1776
2261
  status: "connected",
1777
2262
  config: {
1778
- serverUrl: credentials.serverUrl,
2263
+ serverUrl: credentials.provider === "macos_local"
2264
+ ? "forge-macos-local://eventkit/"
2265
+ : credentials.serverUrl,
1779
2266
  selectedCalendarCount: credentials.selectedCalendarUrls.length,
1780
2267
  ...(credentials.provider === "microsoft"
1781
2268
  ? {
@@ -1783,9 +2270,17 @@ export async function syncCalendarConnection(connectionId, secrets, activity = {
1783
2270
  tenantId: credentials.tenantId,
1784
2271
  writeMode: "read_only"
1785
2272
  }
1786
- : {
1787
- forgeCalendarUrl: normalizeUrl(credentials.forgeCalendarUrl)
1788
- })
2273
+ : credentials.provider === "macos_local"
2274
+ ? {
2275
+ transportKind: "macos_local",
2276
+ sourceId: credentials.sourceId,
2277
+ sourceType: credentials.sourceType,
2278
+ accountIdentityKey: credentials.accountIdentityKey,
2279
+ forgeCalendarUrl: normalizeOptionalUrl(credentials.forgeCalendarUrl)
2280
+ }
2281
+ : {
2282
+ forgeCalendarUrl: normalizeOptionalUrl(credentials.forgeCalendarUrl)
2283
+ })
1789
2284
  },
1790
2285
  lastSyncedAt: new Date().toISOString(),
1791
2286
  lastSyncError: null
@@ -1793,7 +2288,9 @@ export async function syncCalendarConnection(connectionId, secrets, activity = {
1793
2288
  await publishTaskTimeboxes(state, forgeCalendarUrl, connectionId);
1794
2289
  recordCalendarActivity("calendar_connection_synced", "calendar_connection", connectionId, `Calendar synced: ${connection.label}`, credentials.provider === "microsoft"
1795
2290
  ? "Exchange Online calendars were mirrored into Forge in read-only mode."
1796
- : "Provider events and Forge timeboxes were synchronized.", activity);
2291
+ : forgeCalendarUrl
2292
+ ? "Provider events and Forge timeboxes were synchronized."
2293
+ : "Provider events were synchronized. Forge keeps publishing work blocks and timeboxes through the existing shared write target.", activity);
1797
2294
  return getCalendarConnectionById(connectionId);
1798
2295
  }
1799
2296
  catch (error) {
@@ -1805,6 +2302,7 @@ export async function syncCalendarConnection(connectionId, secrets, activity = {
1805
2302
  }
1806
2303
  }
1807
2304
  export async function updateCalendarConnectionSelection(connectionId, input, secrets, activity = { source: "ui" }) {
2305
+ requireActiveConnection(connectionId);
1808
2306
  const connection = getCalendarConnectionById(connectionId);
1809
2307
  if (!connection) {
1810
2308
  throw new Error(`Unknown calendar connection ${connectionId}`);
@@ -1875,6 +2373,42 @@ export async function syncForgeCalendarEvent(eventId, secrets) {
1875
2373
  continue;
1876
2374
  }
1877
2375
  const { connection, state } = await resolveProviderStateForConnection(source.connectionId, secrets);
2376
+ if (state.mode === "macos_local") {
2377
+ const localCalendar = getCalendarById(source.calendarId);
2378
+ if (!localCalendar || localCalendar.canWrite === false) {
2379
+ continue;
2380
+ }
2381
+ const hostCalendarId = localCalendar.hostCalendarId ??
2382
+ parseMacOSLocalCalendarUrl(localCalendar.remoteId).calendarId;
2383
+ const { event: remoteEvent } = await upsertMacOSLocalEvent({
2384
+ calendarId: hostCalendarId,
2385
+ eventId: source.remoteEventId,
2386
+ title: event.title,
2387
+ startAt: event.start_at,
2388
+ endAt: event.end_at,
2389
+ notes: event.description,
2390
+ location: event.location,
2391
+ allDay: Boolean(event.is_all_day)
2392
+ });
2393
+ registerCalendarEventSourceProjection({
2394
+ forgeEventId: eventId,
2395
+ provider: connection.provider,
2396
+ connectionId: connection.id,
2397
+ calendarId: source.calendarId,
2398
+ remoteCalendarId: getCalendarById(source.calendarId)?.remoteId ?? null,
2399
+ remoteEventId: remoteEvent.eventId,
2400
+ remoteUid: remoteEvent.externalId,
2401
+ recurrenceInstanceId: remoteEvent.occurrenceDate,
2402
+ isMasterRecurring: false,
2403
+ syncState: "synced",
2404
+ rawPayloadJson: JSON.stringify({
2405
+ externalId: remoteEvent.externalId,
2406
+ occurrenceDate: remoteEvent.occurrenceDate
2407
+ }),
2408
+ lastSyncedAt: new Date().toISOString()
2409
+ });
2410
+ continue;
2411
+ }
1878
2412
  if (state.mode !== "dav") {
1879
2413
  continue;
1880
2414
  }
@@ -1921,6 +2455,40 @@ export async function syncForgeCalendarEvent(eventId, secrets) {
1921
2455
  return;
1922
2456
  }
1923
2457
  const { connection, state } = await resolveProviderStateForConnection(event.preferred_connection_id, secrets);
2458
+ if (state.mode === "macos_local") {
2459
+ const localCalendar = getCalendarById(event.preferred_calendar_id);
2460
+ if (!localCalendar || localCalendar.canWrite === false) {
2461
+ throw new Error(`Unknown local calendar for event ${eventId}`);
2462
+ }
2463
+ const hostCalendarId = localCalendar.hostCalendarId ??
2464
+ parseMacOSLocalCalendarUrl(localCalendar.remoteId).calendarId;
2465
+ const { event: remoteEvent } = await upsertMacOSLocalEvent({
2466
+ calendarId: hostCalendarId,
2467
+ title: event.title,
2468
+ startAt: event.start_at,
2469
+ endAt: event.end_at,
2470
+ notes: event.description,
2471
+ location: event.location,
2472
+ allDay: Boolean(event.is_all_day)
2473
+ });
2474
+ registerCalendarEventSourceProjection({
2475
+ forgeEventId: eventId,
2476
+ provider: connection.provider,
2477
+ connectionId: connection.id,
2478
+ calendarId: event.preferred_calendar_id,
2479
+ remoteCalendarId: getCalendarById(event.preferred_calendar_id)?.remoteId ?? null,
2480
+ remoteEventId: remoteEvent.eventId,
2481
+ remoteUid: remoteEvent.externalId,
2482
+ recurrenceInstanceId: remoteEvent.occurrenceDate,
2483
+ syncState: "synced",
2484
+ rawPayloadJson: JSON.stringify({
2485
+ externalId: remoteEvent.externalId,
2486
+ occurrenceDate: remoteEvent.occurrenceDate
2487
+ }),
2488
+ lastSyncedAt: new Date().toISOString()
2489
+ });
2490
+ return;
2491
+ }
1924
2492
  if (state.mode !== "dav") {
1925
2493
  throw new Error(`Connection ${connection.id} is read-only, so Forge cannot publish this event there.`);
1926
2494
  }
@@ -1966,6 +2534,10 @@ export async function deleteCalendarEventProjection(eventId, secrets) {
1966
2534
  continue;
1967
2535
  }
1968
2536
  const { connection, state } = await resolveProviderStateForConnection(source.connectionId, secrets);
2537
+ if (state.mode === "macos_local") {
2538
+ await deleteMacOSLocalEvent(source.remoteEventId);
2539
+ continue;
2540
+ }
1969
2541
  if (state.mode !== "dav") {
1970
2542
  continue;
1971
2543
  }
@@ -2010,9 +2582,18 @@ export function listCalendarProviderMetadata() {
2010
2582
  label: "Custom CalDAV",
2011
2583
  supportsDedicatedForgeCalendar: true,
2012
2584
  connectionHelp: "Use a CalDAV base server URL plus account credentials. Forge discovers the calendars available under that account before you pick what to sync."
2585
+ },
2586
+ {
2587
+ provider: "macos_local",
2588
+ label: "Calendars On This Mac",
2589
+ supportsDedicatedForgeCalendar: true,
2590
+ connectionHelp: "Use EventKit to access the calendars already configured in Calendar.app on this Mac. When the same account is already connected remotely, Forge replaces the older connection instead of showing duplicate copies."
2013
2591
  }
2014
2592
  ];
2015
2593
  }
2016
2594
  export function listConnectedCalendarConnections() {
2017
- return listCalendarConnections().map(({ credentialsSecretId: _secret, ...connection }) => connection);
2595
+ return listCalendarConnections()
2596
+ .filter((connection) => !(typeof connection.config.replacedByConnectionId === "string" &&
2597
+ connection.config.replacedByConnectionId.trim().length > 0))
2598
+ .map(({ credentialsSecretId: _secret, ...connection }) => connection);
2018
2599
  }