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
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-D1H7myQH.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-Cwf49UMz.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/board-DPFvZf-D.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-GXFcgvSw.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-Bvwc85ch.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/table-FJQTJvUR.js">
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-Auw3JrdE.css">
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 to the dedicated Forge calendar for that connection."
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 into the dedicated Forge calendar during provider sync.",
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
- const connection = await syncCalendarConnection(id, managers.secrets, toActivityContext(auth));
8142
- if (!connection) {
8143
- reply.code(404);
8144
- return { error: "Calendar connection not found" };
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 dedicated Forge write calendar.",
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
- return rows.map(mapCalendar);
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 = ?, last_synced_at = ?, updated_at = ?
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, last_synced_at, created_at, updated_at
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
- return filterOwnedEntities("calendar_event", rows.map(mapEvent), query.userIds);
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" ? String(input.rawPayload.uid) : null,
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
- : null,
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" ? String(input.rawPayload.uid) : null,
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
- : null,
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
- return filterOwnedEntities("task_timebox", rows.map(mapTimebox), query.userIds);
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),