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.
- package/README.md +1 -1
- package/dist/assets/{board-DPFvZf-D.js → board-q8cfwaAW.js} +2 -2
- package/dist/assets/{board-DPFvZf-D.js.map → board-q8cfwaAW.js.map} +1 -1
- package/dist/assets/index-C6PCeHD_.css +1 -0
- package/dist/assets/index-bfHIqj0-.js +85 -0
- package/dist/assets/index-bfHIqj0-.js.map +1 -0
- package/dist/assets/{motion-Bvwc85ch.js → motion-DHfqFntt.js} +2 -2
- package/dist/assets/{motion-Bvwc85ch.js.map → motion-DHfqFntt.js.map} +1 -1
- package/dist/assets/{table-FJQTJvUR.js → table-DLweENXt.js} +2 -2
- package/dist/assets/{table-FJQTJvUR.js.map → table-DLweENXt.js.map} +1 -1
- package/dist/assets/{ui-GXFcgvSw.js → ui-BV0OYxkH.js} +2 -2
- package/dist/assets/{ui-GXFcgvSw.js.map → ui-BV0OYxkH.js.map} +1 -1
- package/dist/assets/{vendor-Cwf49UMz.js → vendor-OwcH20PM.js} +2 -2
- package/dist/assets/{vendor-Cwf49UMz.js.map → vendor-OwcH20PM.js.map} +1 -1
- package/dist/index.html +7 -7
- package/dist/server/server/migrations/044_macos_local_calendar_provider.sql +21 -0
- package/dist/server/server/src/app.js +87 -12
- package/dist/server/server/src/openapi.js +29 -1
- package/dist/server/server/src/repositories/calendar.js +144 -12
- package/dist/server/server/src/repositories/tasks.js +36 -17
- package/dist/server/server/src/services/calendar-runtime.js +613 -32
- package/dist/server/server/src/services/macos-calendar-helper.js +748 -0
- package/dist/server/server/src/types.js +46 -2
- package/dist/server/src/lib/api-error.js +2 -0
- package/dist/server/src/lib/api.js +39 -2
- package/dist/server/src/lib/calendar-name-deduper.js +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/044_macos_local_calendar_provider.sql +21 -0
- package/skills/forge-openclaw/SKILL.md +21 -5
- package/skills/forge-openclaw/entity_conversation_playbooks.md +88 -5
- package/dist/assets/index-Auw3JrdE.css +0 -1
- package/dist/assets/index-D1H7myQH.js +0 -85
- package/dist/assets/index-D1H7myQH.js.map +0 -1
package/dist/index.html
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
/>
|
|
14
14
|
<link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
15
15
|
<link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
|
|
16
|
-
<script type="module" crossorigin src="/forge/assets/index-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/board-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/ui-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/motion-
|
|
21
|
-
<link rel="modulepreload" crossorigin href="/forge/assets/table-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-bfHIqj0-.js"></script>
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/vendor-OwcH20PM.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/board-q8cfwaAW.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/ui-BV0OYxkH.js">
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/motion-DHfqFntt.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/forge/assets/table-DLweENXt.js">
|
|
22
22
|
<link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
|
|
23
|
-
<link rel="stylesheet" crossorigin href="/forge/assets/index-
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/forge/assets/index-C6PCeHD_.css">
|
|
24
24
|
</head>
|
|
25
25
|
<body class="bg-canvas text-ink antialiased">
|
|
26
26
|
<div id="root"></div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ALTER TABLE calendar_calendars
|
|
2
|
+
ADD COLUMN source_id TEXT;
|
|
3
|
+
|
|
4
|
+
ALTER TABLE calendar_calendars
|
|
5
|
+
ADD COLUMN source_title TEXT;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE calendar_calendars
|
|
8
|
+
ADD COLUMN source_type TEXT;
|
|
9
|
+
|
|
10
|
+
ALTER TABLE calendar_calendars
|
|
11
|
+
ADD COLUMN calendar_type TEXT;
|
|
12
|
+
|
|
13
|
+
ALTER TABLE calendar_calendars
|
|
14
|
+
ADD COLUMN host_calendar_id TEXT;
|
|
15
|
+
|
|
16
|
+
ALTER TABLE calendar_calendars
|
|
17
|
+
ADD COLUMN canonical_key TEXT;
|
|
18
|
+
|
|
19
|
+
UPDATE calendar_calendars
|
|
20
|
+
SET canonical_key = remote_id
|
|
21
|
+
WHERE canonical_key IS NULL OR TRIM(canonical_key) = '';
|
|
@@ -48,7 +48,7 @@ import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
|
48
48
|
import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
|
|
49
49
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
50
50
|
import { suggestTags } from "./services/tagging.js";
|
|
51
|
-
import { CalendarConnectionConflictError, completeGoogleCalendarOauth, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getGoogleCalendarOauthSession, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startGoogleCalendarOauth, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
|
|
51
|
+
import { CalendarConnectionConflictError, CalendarConnectionOverlapError, completeGoogleCalendarOauth, completeMicrosoftCalendarOauth, createCalendarConnection, discoverMacOSLocalCalendarSources, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getGoogleCalendarOauthSession, getMacOSLocalCalendarAccessStatus, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, requestMacOSLocalCalendarAccess, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startGoogleCalendarOauth, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
|
|
52
52
|
import { consumeOpenAiCodexOauthCredentials, getOpenAiCodexOauthSession, startOpenAiCodexOauthSession, submitOpenAiCodexOauthManualInput } from "./services/openai-codex-oauth.js";
|
|
53
53
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
54
54
|
import { createQuestionnaireInstrumentSchema, publishQuestionnaireVersionSchema, startQuestionnaireRunSchema, updateQuestionnaireRunSchema, updateQuestionnaireVersionSchema } from "./questionnaire-types.js";
|
|
@@ -2704,10 +2704,12 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
|
|
|
2704
2704
|
"Ask only for what is missing or unclear instead of walking the user through every optional field.",
|
|
2705
2705
|
"Start by saying what seems to matter here or what the record is becoming, then ask the next useful question.",
|
|
2706
2706
|
"Whenever possible, make the direction of the intake visible before the question by naming what you think the user is trying to preserve, clarify, decide, schedule, or make easier.",
|
|
2707
|
+
"When the user's operation is not already explicit, identify the job first: add, update, review, compare, navigate, link, or run.",
|
|
2707
2708
|
"Before each question, decide the one missing thing you are trying to clarify and why it matters for the record.",
|
|
2708
2709
|
"Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
|
|
2709
2710
|
"Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
|
|
2710
2711
|
"One focused question is the default. Only stack a second question when both serve the same clarification job and the user is steady enough for it.",
|
|
2712
|
+
"When the user wants review, comparison, or navigation around an existing record, ask what they are trying to understand first and route to the read path before reopening create or update intake.",
|
|
2711
2713
|
"If the user already answered the normal opening question, do not repeat it. Move to the next missing clarification.",
|
|
2712
2714
|
"Do not over-therapize logistical entities. For tasks, calendar events, work blocks, timeboxes, and task runs, one brief confirming sentence plus one question is usually enough.",
|
|
2713
2715
|
"After each substantive answer, briefly say what is becoming clearer and ask only for the next thing that still changes the record shape or usefulness.",
|
|
@@ -3028,6 +3030,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3028
3030
|
"Ask whether the focus is a stay, a trip, a place, a timeline window, or a selected span.",
|
|
3029
3031
|
"Ask for the time window, place, or movement item that makes the question concrete.",
|
|
3030
3032
|
"Ask what they are trying to notice, preserve, or answer through that movement context.",
|
|
3033
|
+
"Skip the meta lane question when the user already named the exact correction or review target and only one ambiguity remains.",
|
|
3031
3034
|
"If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
|
|
3032
3035
|
"When the user already gave a concrete correction like 'I stayed home during that missing block', confirm only the interval or place if needed, then create the overlay and read the timeline back."
|
|
3033
3036
|
]
|
|
@@ -3040,6 +3043,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3040
3043
|
"Ask whether the job is overview, profile change, weekday-template change, or fatigue signaling.",
|
|
3041
3044
|
"Ask what part of the current energy picture feels most important or inaccurate.",
|
|
3042
3045
|
"Ask what should stay true if they are changing profile or template assumptions.",
|
|
3046
|
+
"If the user already named the life-force lane clearly, skip the meta lane question and ask only for the specific weekday, profile field, or signal that still matters.",
|
|
3043
3047
|
"Route to the dedicated life-force path once the lane is clear."
|
|
3044
3048
|
]
|
|
3045
3049
|
},
|
|
@@ -3051,6 +3055,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3051
3055
|
"Ask whether the job is flow discovery, one flow edit, execution, run history, published output, node-level inspection, or latest-node-output lookup.",
|
|
3052
3056
|
"Ask which flow, slug, run, or node the request is about.",
|
|
3053
3057
|
"Ask what the user is trying to learn, repair, or publish through that flow.",
|
|
3058
|
+
"If the user already named the flow and action clearly, skip the meta lane question and ask only for the missing run, node, or output scope.",
|
|
3054
3059
|
"Route to the dedicated workbench route family once the execution lane is clear."
|
|
3055
3060
|
]
|
|
3056
3061
|
},
|
|
@@ -3060,8 +3065,10 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3060
3065
|
coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
|
|
3061
3066
|
askSequence: [
|
|
3062
3067
|
"Ask what kind of moment or incident this label should capture in lived terms.",
|
|
3068
|
+
"Reflect the repeated moment back in plain language before narrowing the wording.",
|
|
3063
3069
|
"Ask how narrow or broad it should be.",
|
|
3064
3070
|
"Ask what would count as inside versus outside the category if that boundary is still fuzzy.",
|
|
3071
|
+
"Offer a concise label if the lived meaning is clearer than the wording.",
|
|
3065
3072
|
"Ask for a short description only if the label could be ambiguous later."
|
|
3066
3073
|
]
|
|
3067
3074
|
},
|
|
@@ -3071,7 +3078,9 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
3071
3078
|
coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
|
|
3072
3079
|
askSequence: [
|
|
3073
3080
|
"Ask what this feeling is like in lived terms when the user says it.",
|
|
3081
|
+
"Reflect the felt signature back in plain language before you settle the label.",
|
|
3074
3082
|
"Ask what distinguishes it from nearby emotions if that matters.",
|
|
3083
|
+
"Offer a concise label if the felt meaning is clearer than the wording.",
|
|
3075
3084
|
"Ask for a broader category only if it will help later browsing or reporting."
|
|
3076
3085
|
]
|
|
3077
3086
|
}
|
|
@@ -3618,17 +3627,18 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3618
3627
|
},
|
|
3619
3628
|
{
|
|
3620
3629
|
toolName: "forge_connect_calendar_provider",
|
|
3621
|
-
summary: "Create a Forge calendar connection for Google, Apple, Exchange Online, or custom CalDAV.",
|
|
3630
|
+
summary: "Create a Forge calendar connection for Google, Apple, Exchange Online, calendars already configured on this Mac, or custom CalDAV.",
|
|
3622
3631
|
whenToUse: "Use only when the operator explicitly wants Forge connected to an external calendar provider.",
|
|
3623
|
-
inputShape: '{ provider: "google"|"apple"|"caldav"|"microsoft", label: string, username?: string, password?: string, serverUrl?: string, authSessionId?: string, selectedCalendarUrls: string[], forgeCalendarUrl?: string, createForgeCalendar?: boolean }',
|
|
3632
|
+
inputShape: '{ provider: "google"|"apple"|"caldav"|"microsoft"|"macos_local", label: string, username?: string, password?: string, serverUrl?: string, authSessionId?: string, sourceId?: string, selectedCalendarUrls: string[], forgeCalendarUrl?: string, createForgeCalendar?: boolean, replaceConnectionIds?: string[] }',
|
|
3624
3633
|
requiredFields: ["provider", "label", "provider-specific credentials"],
|
|
3625
3634
|
notes: [
|
|
3626
3635
|
"Google now uses an interactive localhost Authorization Code + PKCE flow. The user signs in interactively on the same machine running Forge, Forge exchanges the authorization code on the backend, and forge_connect_calendar_provider should only be used after a completed Google authSessionId exists.",
|
|
3627
3636
|
"Apple starts from https://caldav.icloud.com and autodiscovers the principal plus calendars after authentication.",
|
|
3628
3637
|
"Exchange Online uses Microsoft Graph. In the current Forge implementation it is read-only: Forge mirrors the selected calendars but does not publish work blocks or timeboxes back to Microsoft.",
|
|
3629
3638
|
"In the current self-hosted local runtime, Exchange Online now uses an interactive Microsoft public-client sign-in flow with PKCE after the operator has saved the Microsoft client ID, tenant, and redirect URI in Settings -> Calendar. Non-interactive callers should treat Microsoft connection setup as a Settings-owned operator action unless a completed authSessionId already exists.",
|
|
3639
|
+
"macos_local uses EventKit to read and write the calendars already configured on the host Mac. Discovery is grouped by host calendar source, and Forge replaces overlapping remote connections for the same account instead of keeping duplicate copies.",
|
|
3630
3640
|
"Custom CalDAV uses an account-level server URL, not a single calendar collection URL.",
|
|
3631
|
-
"Writable providers publish Forge work blocks and timeboxes
|
|
3641
|
+
"Writable providers publish Forge work blocks and timeboxes through one shared Forge write target. A new connection only needs its own write calendar when the runtime does not already have one."
|
|
3632
3642
|
],
|
|
3633
3643
|
example: '{"provider":"apple","label":"Primary Apple","username":"operator@example.com","password":"app-password","selectedCalendarUrls":["https://caldav.icloud.com/.../Family/"],"forgeCalendarUrl":"https://caldav.icloud.com/.../Forge/","createForgeCalendar":false}'
|
|
3634
3644
|
},
|
|
@@ -3674,7 +3684,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
3674
3684
|
inputShape: '{ taskId: string, projectId?: string|null, title: string, startsAt: string, endsAt: string, source?: "manual"|"suggested"|"live_run" }',
|
|
3675
3685
|
requiredFields: ["taskId", "title", "startsAt", "endsAt"],
|
|
3676
3686
|
notes: [
|
|
3677
|
-
"Forge publishes these
|
|
3687
|
+
"Forge publishes these through the shared Forge write target during provider sync.",
|
|
3678
3688
|
"Live task runs can later attach to matching timeboxes.",
|
|
3679
3689
|
"This is a convenience helper; agents can also create task_timebox through forge_create_entities."
|
|
3680
3690
|
],
|
|
@@ -4147,9 +4157,23 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4147
4157
|
sleepOverview: "/api/v1/health/sleep",
|
|
4148
4158
|
sportsOverview: "/api/v1/health/fitness",
|
|
4149
4159
|
lifeForce: "/api/v1/life-force",
|
|
4160
|
+
lifeForceProfile: "/api/v1/life-force/profile",
|
|
4161
|
+
lifeForceWeekdayTemplate: "/api/v1/life-force/templates/:weekday",
|
|
4162
|
+
lifeForceFatigueSignals: "/api/v1/life-force/fatigue-signals",
|
|
4163
|
+
movementDay: "/api/v1/movement/day",
|
|
4164
|
+
movementMonth: "/api/v1/movement/month",
|
|
4150
4165
|
movementTimeline: "/api/v1/movement/timeline",
|
|
4151
4166
|
movementAllTime: "/api/v1/movement/all-time",
|
|
4167
|
+
movementPlaces: "/api/v1/movement/places",
|
|
4168
|
+
movementTripDetail: "/api/v1/movement/trips/:id",
|
|
4169
|
+
movementSelection: "/api/v1/movement/selection",
|
|
4170
|
+
movementUserBoxPreflight: "/api/v1/movement/user-boxes/preflight",
|
|
4152
4171
|
workbenchFlows: "/api/v1/workbench/flows",
|
|
4172
|
+
workbenchFlowBySlug: "/api/v1/workbench/flows/by-slug/:slug",
|
|
4173
|
+
workbenchPublishedOutput: "/api/v1/workbench/flows/:id/output",
|
|
4174
|
+
workbenchRunDetail: "/api/v1/workbench/flows/:id/runs/:runId",
|
|
4175
|
+
workbenchNodeResult: "/api/v1/workbench/flows/:id/runs/:runId/nodes/:nodeId",
|
|
4176
|
+
workbenchLatestNodeOutput: "/api/v1/workbench/flows/:id/nodes/:nodeId/output",
|
|
4153
4177
|
wikiSettings: "/api/v1/wiki/settings",
|
|
4154
4178
|
wikiSearch: "/api/v1/wiki/search",
|
|
4155
4179
|
wikiHealth: "/api/v1/wiki/health",
|
|
@@ -7270,6 +7294,36 @@ export async function buildServer(options = {}) {
|
|
|
7270
7294
|
providers: listCalendarProviderMetadata(),
|
|
7271
7295
|
connections: listConnectedCalendarConnections()
|
|
7272
7296
|
}));
|
|
7297
|
+
app.get("/api/v1/calendar/macos-local/status", async (request) => {
|
|
7298
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7299
|
+
route: "/api/v1/calendar/macos-local/status"
|
|
7300
|
+
});
|
|
7301
|
+
return await getMacOSLocalCalendarAccessStatus();
|
|
7302
|
+
});
|
|
7303
|
+
app.post("/api/v1/calendar/macos-local/request-access", async (request) => {
|
|
7304
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7305
|
+
route: "/api/v1/calendar/macos-local/request-access"
|
|
7306
|
+
});
|
|
7307
|
+
return await requestMacOSLocalCalendarAccess();
|
|
7308
|
+
});
|
|
7309
|
+
app.get("/api/v1/calendar/macos-local/discovery", async (request) => {
|
|
7310
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/macos-local/discovery" });
|
|
7311
|
+
const discovery = await discoverMacOSLocalCalendarSources();
|
|
7312
|
+
recordActivityEvent({
|
|
7313
|
+
entityType: "calendar_connection",
|
|
7314
|
+
entityId: "calendar_discovery_macos_local",
|
|
7315
|
+
eventType: "calendar_connection_discovered",
|
|
7316
|
+
title: "Calendar discovery completed for macOS local calendars",
|
|
7317
|
+
description: "Forge discovered the calendars already configured on this Mac before connection setup.",
|
|
7318
|
+
actor: auth.actor ?? null,
|
|
7319
|
+
source: auth.source,
|
|
7320
|
+
metadata: {
|
|
7321
|
+
provider: "macos_local",
|
|
7322
|
+
sources: discovery.sources.length
|
|
7323
|
+
}
|
|
7324
|
+
});
|
|
7325
|
+
return { discovery };
|
|
7326
|
+
});
|
|
7273
7327
|
app.post("/api/v1/calendar/oauth/google/start", async (request) => {
|
|
7274
7328
|
requireScopedAccess(request.headers, ["write"], {
|
|
7275
7329
|
route: "/api/v1/calendar/oauth/google/start"
|
|
@@ -8104,6 +8158,14 @@ export async function buildServer(options = {}) {
|
|
|
8104
8158
|
existingConnectionId: error.connectionId
|
|
8105
8159
|
};
|
|
8106
8160
|
}
|
|
8161
|
+
if (error instanceof CalendarConnectionOverlapError) {
|
|
8162
|
+
reply.code(409);
|
|
8163
|
+
return {
|
|
8164
|
+
code: "calendar_connection_overlap",
|
|
8165
|
+
error: error.message,
|
|
8166
|
+
overlappingConnectionIds: error.connectionIds
|
|
8167
|
+
};
|
|
8168
|
+
}
|
|
8107
8169
|
throw error;
|
|
8108
8170
|
}
|
|
8109
8171
|
});
|
|
@@ -8138,14 +8200,27 @@ export async function buildServer(options = {}) {
|
|
|
8138
8200
|
app.post("/api/v1/calendar/connections/:id/sync", async (request, reply) => {
|
|
8139
8201
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/sync" });
|
|
8140
8202
|
const { id } = request.params;
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8203
|
+
try {
|
|
8204
|
+
const connection = await syncCalendarConnection(id, managers.secrets, toActivityContext(auth));
|
|
8205
|
+
if (!connection) {
|
|
8206
|
+
reply.code(404);
|
|
8207
|
+
return { error: "Calendar connection not found" };
|
|
8208
|
+
}
|
|
8209
|
+
return {
|
|
8210
|
+
connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
|
|
8211
|
+
};
|
|
8212
|
+
}
|
|
8213
|
+
catch (error) {
|
|
8214
|
+
if (error instanceof Error &&
|
|
8215
|
+
error.message.includes("replaced by a newer canonical connection")) {
|
|
8216
|
+
reply.code(409);
|
|
8217
|
+
return {
|
|
8218
|
+
code: "calendar_connection_superseded",
|
|
8219
|
+
error: error.message
|
|
8220
|
+
};
|
|
8221
|
+
}
|
|
8222
|
+
throw error;
|
|
8145
8223
|
}
|
|
8146
|
-
return {
|
|
8147
|
-
connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
|
|
8148
|
-
};
|
|
8149
8224
|
});
|
|
8150
8225
|
app.delete("/api/v1/calendar/connections/:id", async (request, reply) => {
|
|
8151
8226
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
|
|
@@ -3003,9 +3003,23 @@ export function buildOpenApiDocument() {
|
|
|
3003
3003
|
"sleepOverview",
|
|
3004
3004
|
"sportsOverview",
|
|
3005
3005
|
"lifeForce",
|
|
3006
|
+
"lifeForceProfile",
|
|
3007
|
+
"lifeForceWeekdayTemplate",
|
|
3008
|
+
"lifeForceFatigueSignals",
|
|
3009
|
+
"movementDay",
|
|
3010
|
+
"movementMonth",
|
|
3006
3011
|
"movementTimeline",
|
|
3007
3012
|
"movementAllTime",
|
|
3013
|
+
"movementPlaces",
|
|
3014
|
+
"movementTripDetail",
|
|
3015
|
+
"movementSelection",
|
|
3016
|
+
"movementUserBoxPreflight",
|
|
3008
3017
|
"workbenchFlows",
|
|
3018
|
+
"workbenchFlowBySlug",
|
|
3019
|
+
"workbenchPublishedOutput",
|
|
3020
|
+
"workbenchRunDetail",
|
|
3021
|
+
"workbenchNodeResult",
|
|
3022
|
+
"workbenchLatestNodeOutput",
|
|
3009
3023
|
"wikiSettings",
|
|
3010
3024
|
"wikiSearch",
|
|
3011
3025
|
"wikiHealth",
|
|
@@ -3023,9 +3037,23 @@ export function buildOpenApiDocument() {
|
|
|
3023
3037
|
sleepOverview: { type: "string" },
|
|
3024
3038
|
sportsOverview: { type: "string" },
|
|
3025
3039
|
lifeForce: { type: "string" },
|
|
3040
|
+
lifeForceProfile: { type: "string" },
|
|
3041
|
+
lifeForceWeekdayTemplate: { type: "string" },
|
|
3042
|
+
lifeForceFatigueSignals: { type: "string" },
|
|
3043
|
+
movementDay: { type: "string" },
|
|
3044
|
+
movementMonth: { type: "string" },
|
|
3026
3045
|
movementTimeline: { type: "string" },
|
|
3027
3046
|
movementAllTime: { type: "string" },
|
|
3047
|
+
movementPlaces: { type: "string" },
|
|
3048
|
+
movementTripDetail: { type: "string" },
|
|
3049
|
+
movementSelection: { type: "string" },
|
|
3050
|
+
movementUserBoxPreflight: { type: "string" },
|
|
3028
3051
|
workbenchFlows: { type: "string" },
|
|
3052
|
+
workbenchFlowBySlug: { type: "string" },
|
|
3053
|
+
workbenchPublishedOutput: { type: "string" },
|
|
3054
|
+
workbenchRunDetail: { type: "string" },
|
|
3055
|
+
workbenchNodeResult: { type: "string" },
|
|
3056
|
+
workbenchLatestNodeOutput: { type: "string" },
|
|
3029
3057
|
wikiSettings: { type: "string" },
|
|
3030
3058
|
wikiSearch: { type: "string" },
|
|
3031
3059
|
wikiHealth: { type: "string" },
|
|
@@ -6776,7 +6804,7 @@ export function buildOpenApiDocument() {
|
|
|
6776
6804
|
},
|
|
6777
6805
|
post: {
|
|
6778
6806
|
summary: "Create a Google, Apple, or custom CalDAV calendar connection",
|
|
6779
|
-
description: "Forge first discovers the writable calendars for the account, then stores the chosen mirrored calendars and
|
|
6807
|
+
description: "Forge first discovers the writable calendars for the account, then stores the chosen mirrored calendars and either reuses the existing shared Forge write target or saves a new one when needed.",
|
|
6780
6808
|
responses: {
|
|
6781
6809
|
"201": jsonResponse({
|
|
6782
6810
|
type: "object",
|
|
@@ -64,6 +64,12 @@ function mapCalendar(row) {
|
|
|
64
64
|
canWrite: Boolean(row.can_write),
|
|
65
65
|
selectedForSync: Boolean(row.selected_for_sync),
|
|
66
66
|
forgeManaged: Boolean(row.forge_managed),
|
|
67
|
+
sourceId: row.source_id,
|
|
68
|
+
sourceTitle: row.source_title,
|
|
69
|
+
sourceType: row.source_type,
|
|
70
|
+
calendarType: row.calendar_type,
|
|
71
|
+
hostCalendarId: row.host_calendar_id,
|
|
72
|
+
canonicalKey: row.canonical_key,
|
|
67
73
|
lastSyncedAt: row.last_synced_at,
|
|
68
74
|
createdAt: row.created_at,
|
|
69
75
|
updatedAt: row.updated_at
|
|
@@ -255,6 +261,13 @@ export function readEncryptedSecret(secretId) {
|
|
|
255
261
|
export function deleteEncryptedSecret(secretId) {
|
|
256
262
|
getDatabase().prepare(`DELETE FROM stored_secrets WHERE id = ?`).run(secretId);
|
|
257
263
|
}
|
|
264
|
+
export function isSupersededCalendarConnection(connectionId) {
|
|
265
|
+
const connection = getCalendarConnectionById(connectionId);
|
|
266
|
+
if (!connection) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
return isSupersededConnection(connection);
|
|
270
|
+
}
|
|
258
271
|
export function listCalendarConnections() {
|
|
259
272
|
const rows = getDatabase()
|
|
260
273
|
.prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
|
|
@@ -264,6 +277,15 @@ export function listCalendarConnections() {
|
|
|
264
277
|
.all();
|
|
265
278
|
return rows.map(mapConnection);
|
|
266
279
|
}
|
|
280
|
+
function isSupersededConnection(connection) {
|
|
281
|
+
return (typeof connection.config.replacedByConnectionId === "string" &&
|
|
282
|
+
connection.config.replacedByConnectionId.trim().length > 0);
|
|
283
|
+
}
|
|
284
|
+
function activeConnectionIds() {
|
|
285
|
+
return new Set(listCalendarConnections()
|
|
286
|
+
.filter((connection) => !isSupersededConnection(connection))
|
|
287
|
+
.map((connection) => connection.id));
|
|
288
|
+
}
|
|
267
289
|
export function getCalendarConnectionById(connectionId) {
|
|
268
290
|
const row = getDatabase()
|
|
269
291
|
.prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
|
|
@@ -330,6 +352,70 @@ export function deleteExternalEventsForConnection(connectionId) {
|
|
|
330
352
|
}
|
|
331
353
|
return rows.map((row) => row.id);
|
|
332
354
|
}
|
|
355
|
+
export function rehomeCalendarConnectionReferences(input) {
|
|
356
|
+
return runInTransaction(() => {
|
|
357
|
+
const fromCalendars = listCalendars(input.fromConnectionId, {
|
|
358
|
+
includeUnselected: true
|
|
359
|
+
});
|
|
360
|
+
const toCalendars = listCalendars(input.toConnectionId, {
|
|
361
|
+
includeUnselected: true
|
|
362
|
+
});
|
|
363
|
+
const toForgeCalendar = toCalendars.find((calendar) => calendar.forgeManaged) ??
|
|
364
|
+
toCalendars.find((calendar) => calendar.canWrite) ??
|
|
365
|
+
null;
|
|
366
|
+
const toByCanonicalKey = new Map(toCalendars
|
|
367
|
+
.filter((calendar) => typeof calendar.canonicalKey === "string" &&
|
|
368
|
+
calendar.canonicalKey.trim().length > 0)
|
|
369
|
+
.map((calendar) => [calendar.canonicalKey, calendar]));
|
|
370
|
+
const mappedCalendarIds = new Map();
|
|
371
|
+
for (const fromCalendar of fromCalendars) {
|
|
372
|
+
const mapped = (fromCalendar.canonicalKey
|
|
373
|
+
? toByCanonicalKey.get(fromCalendar.canonicalKey)
|
|
374
|
+
: null) ??
|
|
375
|
+
(fromCalendar.forgeManaged ? toForgeCalendar : null) ??
|
|
376
|
+
null;
|
|
377
|
+
mappedCalendarIds.set(fromCalendar.id, mapped?.id ?? null);
|
|
378
|
+
}
|
|
379
|
+
const now = nowIso();
|
|
380
|
+
const forgeEventRows = getDatabase()
|
|
381
|
+
.prepare(`SELECT id, preferred_calendar_id
|
|
382
|
+
FROM forge_events
|
|
383
|
+
WHERE ownership = 'forge' AND preferred_connection_id = ?`)
|
|
384
|
+
.all(input.fromConnectionId);
|
|
385
|
+
const updateForgeEvent = getDatabase().prepare(`UPDATE forge_events
|
|
386
|
+
SET preferred_connection_id = ?, preferred_calendar_id = ?, updated_at = ?
|
|
387
|
+
WHERE id = ?`);
|
|
388
|
+
for (const row of forgeEventRows) {
|
|
389
|
+
const nextCalendarId = row.preferred_calendar_id
|
|
390
|
+
? (mappedCalendarIds.get(row.preferred_calendar_id) ?? toForgeCalendar?.id ?? null)
|
|
391
|
+
: (toForgeCalendar?.id ?? null);
|
|
392
|
+
updateForgeEvent.run(nextCalendarId ? input.toConnectionId : null, nextCalendarId, now, row.id);
|
|
393
|
+
}
|
|
394
|
+
const timeboxRows = getDatabase()
|
|
395
|
+
.prepare(`SELECT id, calendar_id
|
|
396
|
+
FROM task_timeboxes
|
|
397
|
+
WHERE connection_id = ?`)
|
|
398
|
+
.all(input.fromConnectionId);
|
|
399
|
+
const updateTimebox = getDatabase().prepare(`UPDATE task_timeboxes
|
|
400
|
+
SET connection_id = ?, calendar_id = ?, remote_event_id = NULL, updated_at = ?
|
|
401
|
+
WHERE id = ?`);
|
|
402
|
+
for (const row of timeboxRows) {
|
|
403
|
+
const nextCalendarId = row.calendar_id
|
|
404
|
+
? (mappedCalendarIds.get(row.calendar_id) ?? toForgeCalendar?.id ?? null)
|
|
405
|
+
: (toForgeCalendar?.id ?? null);
|
|
406
|
+
updateTimebox.run(nextCalendarId ? input.toConnectionId : null, nextCalendarId, now, row.id);
|
|
407
|
+
}
|
|
408
|
+
getDatabase()
|
|
409
|
+
.prepare(`DELETE FROM forge_event_sources
|
|
410
|
+
WHERE connection_id = ?
|
|
411
|
+
AND forge_event_id IN (
|
|
412
|
+
SELECT id
|
|
413
|
+
FROM forge_events
|
|
414
|
+
WHERE ownership = 'forge'
|
|
415
|
+
)`)
|
|
416
|
+
.run(input.fromConnectionId);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
333
419
|
export function detachConnectionFromForgeEvents(connectionId) {
|
|
334
420
|
const now = nowIso();
|
|
335
421
|
getDatabase()
|
|
@@ -352,16 +438,23 @@ export function listCalendars(connectionId, options = {}) {
|
|
|
352
438
|
: "WHERE (selected_for_sync = 1 OR forge_managed = 1)";
|
|
353
439
|
const rows = getDatabase()
|
|
354
440
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
441
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
355
442
|
last_synced_at, created_at, updated_at
|
|
356
443
|
FROM calendar_calendars
|
|
357
444
|
${connectionId ? `WHERE connection_id = ? ${visibilityClause}` : visibilityClause}
|
|
358
445
|
ORDER BY forge_managed DESC, title ASC`)
|
|
359
446
|
.all(...(connectionId ? [connectionId] : []));
|
|
360
|
-
|
|
447
|
+
const mapped = rows.map(mapCalendar);
|
|
448
|
+
if (connectionId) {
|
|
449
|
+
return mapped;
|
|
450
|
+
}
|
|
451
|
+
const activeIds = activeConnectionIds();
|
|
452
|
+
return mapped.filter((calendar) => activeIds.has(calendar.connectionId));
|
|
361
453
|
}
|
|
362
454
|
export function getCalendarById(calendarId) {
|
|
363
455
|
const row = getDatabase()
|
|
364
456
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
457
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
365
458
|
last_synced_at, created_at, updated_at
|
|
366
459
|
FROM calendar_calendars
|
|
367
460
|
WHERE id = ?`)
|
|
@@ -371,6 +464,7 @@ export function getCalendarById(calendarId) {
|
|
|
371
464
|
function getDefaultWritableCalendar() {
|
|
372
465
|
const row = getDatabase()
|
|
373
466
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
467
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
374
468
|
last_synced_at, created_at, updated_at
|
|
375
469
|
FROM calendar_calendars
|
|
376
470
|
WHERE can_write = 1
|
|
@@ -383,6 +477,7 @@ function getDefaultWritableCalendar() {
|
|
|
383
477
|
export function getCalendarByRemoteId(connectionId, remoteId) {
|
|
384
478
|
const row = getDatabase()
|
|
385
479
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
480
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
386
481
|
last_synced_at, created_at, updated_at
|
|
387
482
|
FROM calendar_calendars
|
|
388
483
|
WHERE connection_id = ? AND remote_id = ?`)
|
|
@@ -395,18 +490,21 @@ export function upsertCalendarRecord(connectionId, input) {
|
|
|
395
490
|
if (existing) {
|
|
396
491
|
getDatabase()
|
|
397
492
|
.prepare(`UPDATE calendar_calendars
|
|
398
|
-
SET title = ?, description = ?, color = ?, timezone = ?, is_primary = ?, can_write = ?, selected_for_sync = ?, forge_managed = ?,
|
|
493
|
+
SET title = ?, description = ?, color = ?, timezone = ?, is_primary = ?, can_write = ?, selected_for_sync = ?, forge_managed = ?,
|
|
494
|
+
source_id = ?, source_title = ?, source_type = ?, calendar_type = ?, host_calendar_id = ?, canonical_key = ?,
|
|
495
|
+
last_synced_at = ?, updated_at = ?
|
|
399
496
|
WHERE id = ?`)
|
|
400
|
-
.run(input.title, input.description ?? existing.description, input.color ?? existing.color, normalizeTimezone(input.timezone ?? existing.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, now, now, existing.id);
|
|
497
|
+
.run(input.title, input.description ?? existing.description, input.color ?? existing.color, normalizeTimezone(input.timezone ?? existing.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, input.sourceId ?? existing.sourceId, input.sourceTitle ?? existing.sourceTitle, input.sourceType ?? existing.sourceType, input.calendarType ?? existing.calendarType, input.hostCalendarId ?? existing.hostCalendarId, input.canonicalKey ?? existing.canonicalKey ?? existing.remoteId, now, now, existing.id);
|
|
401
498
|
return getCalendarById(existing.id);
|
|
402
499
|
}
|
|
403
500
|
const id = `calendar_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
404
501
|
getDatabase()
|
|
405
502
|
.prepare(`INSERT INTO calendar_calendars (
|
|
406
|
-
id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
503
|
+
id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
504
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key, last_synced_at, created_at, updated_at
|
|
407
505
|
)
|
|
408
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
409
|
-
.run(id, connectionId, input.remoteId, input.title, input.description ?? "", input.color ?? "#7dd3fc", normalizeTimezone(input.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, now, now, now);
|
|
506
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
507
|
+
.run(id, connectionId, input.remoteId, input.title, input.description ?? "", input.color ?? "#7dd3fc", normalizeTimezone(input.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, input.sourceId ?? null, input.sourceTitle ?? null, input.sourceType ?? null, input.calendarType ?? null, input.hostCalendarId ?? null, input.canonicalKey ?? input.remoteId, now, now, now);
|
|
410
508
|
return getCalendarById(id);
|
|
411
509
|
}
|
|
412
510
|
export function listCalendarEvents(query) {
|
|
@@ -442,7 +540,12 @@ export function listCalendarEvents(query) {
|
|
|
442
540
|
WHERE ${clauses.join(" AND ")}
|
|
443
541
|
ORDER BY start_at ASC, title ASC`)
|
|
444
542
|
.all(...params);
|
|
445
|
-
|
|
543
|
+
const activeIds = activeConnectionIds();
|
|
544
|
+
return filterOwnedEntities("calendar_event", rows
|
|
545
|
+
.map(mapEvent)
|
|
546
|
+
.filter((event) => event.ownership !== "external" ||
|
|
547
|
+
event.connectionId === null ||
|
|
548
|
+
activeIds.has(event.connectionId)), query.userIds);
|
|
446
549
|
}
|
|
447
550
|
export function getCalendarEventById(eventId) {
|
|
448
551
|
const row = getDatabase()
|
|
@@ -556,10 +659,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
556
659
|
calendarId: calendar.id,
|
|
557
660
|
remoteCalendarId: calendar.remoteId,
|
|
558
661
|
remoteEventId: input.remoteId,
|
|
559
|
-
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
662
|
+
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
663
|
+
? String(input.rawPayload.uid)
|
|
664
|
+
: typeof input.rawPayload?.externalId === "string"
|
|
665
|
+
? String(input.rawPayload.externalId)
|
|
666
|
+
: typeof input.rawPayload?.iCalUID === "string"
|
|
667
|
+
? String(input.rawPayload.iCalUID)
|
|
668
|
+
: typeof input.rawPayload?.iCalUId === "string"
|
|
669
|
+
? String(input.rawPayload.iCalUId)
|
|
670
|
+
: null,
|
|
560
671
|
recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
|
|
561
672
|
? String(input.rawPayload.recurrenceid)
|
|
562
|
-
:
|
|
673
|
+
: typeof input.rawPayload?.occurrenceDate === "string"
|
|
674
|
+
? String(input.rawPayload.occurrenceDate)
|
|
675
|
+
: null,
|
|
563
676
|
isMasterRecurring: Boolean(input.rawPayload?.rrule),
|
|
564
677
|
remoteHref: input.remoteHref ?? null,
|
|
565
678
|
remoteEtag: input.remoteEtag ?? null,
|
|
@@ -585,10 +698,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
585
698
|
calendarId: calendar.id,
|
|
586
699
|
remoteCalendarId: calendar.remoteId,
|
|
587
700
|
remoteEventId: input.remoteId,
|
|
588
|
-
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
701
|
+
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
702
|
+
? String(input.rawPayload.uid)
|
|
703
|
+
: typeof input.rawPayload?.externalId === "string"
|
|
704
|
+
? String(input.rawPayload.externalId)
|
|
705
|
+
: typeof input.rawPayload?.iCalUID === "string"
|
|
706
|
+
? String(input.rawPayload.iCalUID)
|
|
707
|
+
: typeof input.rawPayload?.iCalUId === "string"
|
|
708
|
+
? String(input.rawPayload.iCalUId)
|
|
709
|
+
: null,
|
|
589
710
|
recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
|
|
590
711
|
? String(input.rawPayload.recurrenceid)
|
|
591
|
-
:
|
|
712
|
+
: typeof input.rawPayload?.occurrenceDate === "string"
|
|
713
|
+
? String(input.rawPayload.occurrenceDate)
|
|
714
|
+
: null,
|
|
592
715
|
isMasterRecurring: Boolean(input.rawPayload?.rrule),
|
|
593
716
|
remoteHref: input.remoteHref ?? null,
|
|
594
717
|
remoteEtag: input.remoteEtag ?? null,
|
|
@@ -931,7 +1054,10 @@ export function listTaskTimeboxes(query) {
|
|
|
931
1054
|
WHERE ${clauses.join(" AND ")}
|
|
932
1055
|
ORDER BY starts_at ASC`)
|
|
933
1056
|
.all(...params);
|
|
934
|
-
|
|
1057
|
+
const activeIds = activeConnectionIds();
|
|
1058
|
+
return filterOwnedEntities("task_timebox", rows
|
|
1059
|
+
.map(mapTimebox)
|
|
1060
|
+
.filter((timebox) => timebox.connectionId === null || activeIds.has(timebox.connectionId)), query.userIds);
|
|
935
1061
|
}
|
|
936
1062
|
export function getTaskTimeboxById(timeboxId) {
|
|
937
1063
|
const row = getDatabase()
|
|
@@ -1302,6 +1428,12 @@ export function getCalendarOverview(query) {
|
|
|
1302
1428
|
label: "Custom CalDAV",
|
|
1303
1429
|
supportsDedicatedForgeCalendar: true,
|
|
1304
1430
|
connectionHelp: "Use an account-level CalDAV base URL, then let Forge discover the calendars before selecting sync and write targets."
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
provider: "macos_local",
|
|
1434
|
+
label: "Calendars On This Mac",
|
|
1435
|
+
supportsDedicatedForgeCalendar: true,
|
|
1436
|
+
connectionHelp: "Use EventKit to access the calendars already configured in Calendar.app on this Mac. Forge replaces overlapping remote account connections instead of showing duplicate copies."
|
|
1305
1437
|
}
|
|
1306
1438
|
],
|
|
1307
1439
|
connections: listCalendarConnections().map(({ credentialsSecretId: _secret, ...connection }) => connection),
|