forge-openclaw-plugin 0.2.28 → 0.2.30

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 (36) 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-CPC6E84V.js +85 -0
  5. package/dist/assets/index-CPC6E84V.js.map +1 -0
  6. package/dist/assets/index-DiyKCDxL.css +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 +113 -17
  18. package/dist/server/server/src/movement.js +151 -0
  19. package/dist/server/server/src/openapi.js +29 -1
  20. package/dist/server/server/src/repositories/calendar.js +144 -12
  21. package/dist/server/server/src/repositories/tasks.js +36 -17
  22. package/dist/server/server/src/services/calendar-runtime.js +613 -32
  23. package/dist/server/server/src/services/life-force.js +84 -52
  24. package/dist/server/server/src/services/macos-calendar-helper.js +748 -0
  25. package/dist/server/server/src/types.js +46 -2
  26. package/dist/server/src/lib/api-error.js +2 -0
  27. package/dist/server/src/lib/api.js +51 -2
  28. package/dist/server/src/lib/calendar-name-deduper.js +2 -0
  29. package/openclaw.plugin.json +1 -1
  30. package/package.json +1 -1
  31. package/server/migrations/044_macos_local_calendar_provider.sql +21 -0
  32. package/skills/forge-openclaw/SKILL.md +40 -7
  33. package/skills/forge-openclaw/entity_conversation_playbooks.md +88 -5
  34. package/dist/assets/index-Auw3JrdE.css +0 -1
  35. package/dist/assets/index-D1H7myQH.js +0 -85
  36. 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-CPC6E84V.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-DiyKCDxL.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";
@@ -60,7 +60,7 @@ import { registerWebRoutes } from "./web.js";
60
60
  import { createManagerRuntime } from "./managers/runtime.js";
61
61
  import { isManagerError } from "./managers/type-guards.js";
62
62
  import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
63
- import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
63
+ import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
64
64
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
65
65
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
66
66
  const COMPATIBILITY_SUNSET = "transitional-node";
@@ -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
  },
@@ -3658,7 +3668,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
3658
3668
  {
3659
3669
  toolName: "forge_recommend_task_timeboxes",
3660
3670
  summary: "Suggest future task slots that fit the current calendar rules and schedule.",
3661
- whenToUse: "Use when preparing focused work in advance.",
3671
+ whenToUse: "Use when preparing focused work in advance and the agent wants Forge to propose candidate slots instead of picking one manually.",
3662
3672
  inputShape: "{ taskId: string, from?: string, to?: string, limit?: integer }",
3663
3673
  requiredFields: ["taskId"],
3664
3674
  notes: [
@@ -3670,15 +3680,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
3670
3680
  {
3671
3681
  toolName: "forge_create_task_timebox",
3672
3682
  summary: "Create a planned task timebox in the Forge calendar domain.",
3673
- whenToUse: "Use after choosing a valid future slot or when creating a manual timebox directly.",
3674
- inputShape: '{ taskId: string, projectId?: string|null, title: string, startsAt: string, endsAt: string, source?: "manual"|"suggested"|"live_run" }',
3683
+ whenToUse: "Use after choosing a valid future slot or, preferably, when the agent has already reasoned over the live calendar and wants to place a manual timebox directly.",
3684
+ inputShape: '{ taskId: string, projectId?: string|null, title: string, startsAt: string, endsAt: string, source?: "manual"|"suggested"|"live_run", overrideReason?: string|null, activityPresetKey?: string|null, customSustainRateApPerHour?: number|null, userId?: string|null }',
3675
3685
  requiredFields: ["taskId", "title", "startsAt", "endsAt"],
3676
3686
  notes: [
3677
- "Forge publishes these into the dedicated Forge calendar during provider sync.",
3687
+ "Manual timeboxing is the main direct path when the agent already understands the calendar and wants to choose the slot itself.",
3688
+ "Forge publishes these through the shared Forge write target during provider sync.",
3678
3689
  "Live task runs can later attach to matching timeboxes.",
3679
3690
  "This is a convenience helper; agents can also create task_timebox through forge_create_entities."
3680
3691
  ],
3681
- example: '{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"suggested"}'
3692
+ example: '{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"manual","overrideReason":"Protected writing block before clinic.","activityPresetKey":"deep_work","customSustainRateApPerHour":6.5}'
3682
3693
  },
3683
3694
  {
3684
3695
  toolName: "forge_grant_reward_bonus",
@@ -4147,9 +4158,23 @@ function buildAgentOnboardingPayload(request) {
4147
4158
  sleepOverview: "/api/v1/health/sleep",
4148
4159
  sportsOverview: "/api/v1/health/fitness",
4149
4160
  lifeForce: "/api/v1/life-force",
4161
+ lifeForceProfile: "/api/v1/life-force/profile",
4162
+ lifeForceWeekdayTemplate: "/api/v1/life-force/templates/:weekday",
4163
+ lifeForceFatigueSignals: "/api/v1/life-force/fatigue-signals",
4164
+ movementDay: "/api/v1/movement/day",
4165
+ movementMonth: "/api/v1/movement/month",
4150
4166
  movementTimeline: "/api/v1/movement/timeline",
4151
4167
  movementAllTime: "/api/v1/movement/all-time",
4168
+ movementPlaces: "/api/v1/movement/places",
4169
+ movementTripDetail: "/api/v1/movement/trips/:id",
4170
+ movementSelection: "/api/v1/movement/selection",
4171
+ movementUserBoxPreflight: "/api/v1/movement/user-boxes/preflight",
4152
4172
  workbenchFlows: "/api/v1/workbench/flows",
4173
+ workbenchFlowBySlug: "/api/v1/workbench/flows/by-slug/:slug",
4174
+ workbenchPublishedOutput: "/api/v1/workbench/flows/:id/output",
4175
+ workbenchRunDetail: "/api/v1/workbench/flows/:id/runs/:runId",
4176
+ workbenchNodeResult: "/api/v1/workbench/flows/:id/runs/:runId/nodes/:nodeId",
4177
+ workbenchLatestNodeOutput: "/api/v1/workbench/flows/:id/nodes/:nodeId/output",
4153
4178
  wikiSettings: "/api/v1/wiki/settings",
4154
4179
  wikiSearch: "/api/v1/wiki/search",
4155
4180
  wikiHealth: "/api/v1/wiki/health",
@@ -5556,6 +5581,15 @@ export async function buildServer(options = {}) {
5556
5581
  }
5557
5582
  return { movement };
5558
5583
  });
5584
+ app.get("/api/v1/movement/boxes/:id", async (request, reply) => {
5585
+ const { id } = request.params;
5586
+ const movement = getMovementBoxDetail(id, resolveScopedUserIds(request.query) ?? []);
5587
+ if (!movement) {
5588
+ reply.code(404);
5589
+ return { error: "Movement box not found" };
5590
+ }
5591
+ return { movement };
5592
+ });
5559
5593
  app.post("/api/v1/movement/selection", async (request) => ({
5560
5594
  movement: getMovementSelectionAggregate(movementSelectionAggregateSchema.parse(request.body ?? {}))
5561
5595
  }));
@@ -5650,6 +5684,17 @@ export async function buildServer(options = {}) {
5650
5684
  })
5651
5685
  };
5652
5686
  });
5687
+ app.post("/api/v1/mobile/movement/boxes/:id/detail", async (request, reply) => {
5688
+ const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
5689
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5690
+ const { id } = request.params;
5691
+ const movement = getMovementBoxDetail(id, [pairing.user_id]);
5692
+ if (!movement) {
5693
+ reply.code(404);
5694
+ return { error: "Movement box not found" };
5695
+ }
5696
+ return { movement };
5697
+ });
5653
5698
  app.post("/api/v1/mobile/movement/user-boxes", async (request, reply) => {
5654
5699
  const parsed = movementMobileUserBoxCreateSchema.parse(request.body ?? {});
5655
5700
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
@@ -7270,6 +7315,36 @@ export async function buildServer(options = {}) {
7270
7315
  providers: listCalendarProviderMetadata(),
7271
7316
  connections: listConnectedCalendarConnections()
7272
7317
  }));
7318
+ app.get("/api/v1/calendar/macos-local/status", async (request) => {
7319
+ requireScopedAccess(request.headers, ["write"], {
7320
+ route: "/api/v1/calendar/macos-local/status"
7321
+ });
7322
+ return await getMacOSLocalCalendarAccessStatus();
7323
+ });
7324
+ app.post("/api/v1/calendar/macos-local/request-access", async (request) => {
7325
+ requireScopedAccess(request.headers, ["write"], {
7326
+ route: "/api/v1/calendar/macos-local/request-access"
7327
+ });
7328
+ return await requestMacOSLocalCalendarAccess();
7329
+ });
7330
+ app.get("/api/v1/calendar/macos-local/discovery", async (request) => {
7331
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/macos-local/discovery" });
7332
+ const discovery = await discoverMacOSLocalCalendarSources();
7333
+ recordActivityEvent({
7334
+ entityType: "calendar_connection",
7335
+ entityId: "calendar_discovery_macos_local",
7336
+ eventType: "calendar_connection_discovered",
7337
+ title: "Calendar discovery completed for macOS local calendars",
7338
+ description: "Forge discovered the calendars already configured on this Mac before connection setup.",
7339
+ actor: auth.actor ?? null,
7340
+ source: auth.source,
7341
+ metadata: {
7342
+ provider: "macos_local",
7343
+ sources: discovery.sources.length
7344
+ }
7345
+ });
7346
+ return { discovery };
7347
+ });
7273
7348
  app.post("/api/v1/calendar/oauth/google/start", async (request) => {
7274
7349
  requireScopedAccess(request.headers, ["write"], {
7275
7350
  route: "/api/v1/calendar/oauth/google/start"
@@ -8104,6 +8179,14 @@ export async function buildServer(options = {}) {
8104
8179
  existingConnectionId: error.connectionId
8105
8180
  };
8106
8181
  }
8182
+ if (error instanceof CalendarConnectionOverlapError) {
8183
+ reply.code(409);
8184
+ return {
8185
+ code: "calendar_connection_overlap",
8186
+ error: error.message,
8187
+ overlappingConnectionIds: error.connectionIds
8188
+ };
8189
+ }
8107
8190
  throw error;
8108
8191
  }
8109
8192
  });
@@ -8138,14 +8221,27 @@ export async function buildServer(options = {}) {
8138
8221
  app.post("/api/v1/calendar/connections/:id/sync", async (request, reply) => {
8139
8222
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/sync" });
8140
8223
  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" };
8224
+ try {
8225
+ const connection = await syncCalendarConnection(id, managers.secrets, toActivityContext(auth));
8226
+ if (!connection) {
8227
+ reply.code(404);
8228
+ return { error: "Calendar connection not found" };
8229
+ }
8230
+ return {
8231
+ connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
8232
+ };
8233
+ }
8234
+ catch (error) {
8235
+ if (error instanceof Error &&
8236
+ error.message.includes("replaced by a newer canonical connection")) {
8237
+ reply.code(409);
8238
+ return {
8239
+ code: "calendar_connection_superseded",
8240
+ error: error.message
8241
+ };
8242
+ }
8243
+ throw error;
8145
8244
  }
8146
- return {
8147
- connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
8148
- };
8149
8245
  });
8150
8246
  app.delete("/api/v1/calendar/connections/:id", async (request, reply) => {
8151
8247
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
@@ -4670,6 +4670,157 @@ export function getMovementTripDetail(tripId) {
4670
4670
  selectionAggregate
4671
4671
  };
4672
4672
  }
4673
+ export function getMovementBoxDetail(boxId, userIds = []) {
4674
+ const scopedUserIds = userIds.length > 0 ? userIds : [getDefaultUser().id];
4675
+ const segment = buildProjectedMovementTimelineSegments(scopedUserIds).find((entry) => entry.boxId === boxId);
4676
+ if (!segment) {
4677
+ return undefined;
4678
+ }
4679
+ const places = listMovementPlaceRows(scopedUserIds).map(mapMovementPlace);
4680
+ const placesById = new Map(places.map((place) => [place.id, place]));
4681
+ const stayRows = listMovementStayRows(scopedUserIds);
4682
+ const rawStays = segment.rawStayIds
4683
+ .map((id) => stayRows.find((row) => row.id === id))
4684
+ .filter((row) => Boolean(row))
4685
+ .map((row) => mapMovementStay(row, placesById));
4686
+ const tripRows = listMovementTripRows(scopedUserIds);
4687
+ const rawTripRows = segment.rawTripIds
4688
+ .map((id) => tripRows.find((row) => row.id === id))
4689
+ .filter((row) => Boolean(row));
4690
+ const rawTripIds = rawTripRows.map((row) => row.id);
4691
+ const pointsByTrip = new Map();
4692
+ listTripPoints(rawTripIds).forEach((point) => {
4693
+ pointsByTrip.set(point.trip_id, [...(pointsByTrip.get(point.trip_id) ?? []), point]);
4694
+ });
4695
+ const stopsByTrip = new Map();
4696
+ listTripStops(rawTripIds).forEach((stop) => {
4697
+ stopsByTrip.set(stop.trip_id, [...(stopsByTrip.get(stop.trip_id) ?? []), stop]);
4698
+ });
4699
+ const rawTrips = rawTripRows.map((row) => mapMovementTrip(row, placesById, pointsByTrip.get(row.id) ?? [], stopsByTrip.get(row.id) ?? []));
4700
+ const stayPositions = rawStays.length > 0
4701
+ ? rawStays.map((stay, index) => ({
4702
+ latitude: stay.centerLatitude,
4703
+ longitude: stay.centerLongitude,
4704
+ recordedAt: stay.startedAt,
4705
+ label: stay.place?.label ?? (stay.label || `Stay ${index + 1}`)
4706
+ }))
4707
+ : segment.kind === "stay" && segment.stay
4708
+ ? [
4709
+ {
4710
+ latitude: segment.stay.centerLatitude,
4711
+ longitude: segment.stay.centerLongitude,
4712
+ recordedAt: segment.stay.startedAt,
4713
+ label: segment.stay.place?.label ?? (segment.stay.label || "Stay")
4714
+ }
4715
+ ]
4716
+ : [];
4717
+ const averageStayPosition = stayPositions.length > 0
4718
+ ? {
4719
+ latitude: round(stayPositions.reduce((sum, position) => sum + position.latitude, 0) /
4720
+ stayPositions.length, 6),
4721
+ longitude: round(stayPositions.reduce((sum, position) => sum + position.longitude, 0) /
4722
+ stayPositions.length, 6),
4723
+ recordedAt: null,
4724
+ label: "Average position"
4725
+ }
4726
+ : null;
4727
+ const stayDetail = segment.kind === "stay"
4728
+ ? {
4729
+ positions: stayPositions,
4730
+ averagePosition: averageStayPosition,
4731
+ canonicalPlace: rawStays[0]?.place ?? segment.stay?.place ?? null,
4732
+ radiusMeters: rawStays.length > 0
4733
+ ? Math.max(...rawStays.map((stay) => stay.radiusMeters))
4734
+ : segment.stay?.radiusMeters ?? null,
4735
+ sampleCount: rawStays.length > 0
4736
+ ? rawStays.reduce((sum, stay) => sum + stay.sampleCount, 0)
4737
+ : segment.stay?.sampleCount ?? 0
4738
+ }
4739
+ : null;
4740
+ const tripPositions = rawTrips.length > 0
4741
+ ? rawTrips
4742
+ .flatMap((trip) => trip.points)
4743
+ .sort((left, right) => Date.parse(left.recordedAt) - Date.parse(right.recordedAt))
4744
+ .map((point, index) => ({
4745
+ latitude: point.latitude,
4746
+ longitude: point.longitude,
4747
+ recordedAt: point.recordedAt,
4748
+ label: index === 0 ? "Start" : null,
4749
+ accuracyMeters: point.accuracyMeters,
4750
+ altitudeMeters: point.altitudeMeters,
4751
+ speedMps: point.speedMps,
4752
+ isStopAnchor: point.isStopAnchor
4753
+ }))
4754
+ : segment.kind === "trip" && segment.trip
4755
+ ? segment.trip.points.map((point, index) => ({
4756
+ latitude: point.latitude,
4757
+ longitude: point.longitude,
4758
+ recordedAt: point.recordedAt,
4759
+ label: index === 0 ? "Start" : null,
4760
+ accuracyMeters: point.accuracyMeters,
4761
+ altitudeMeters: point.altitudeMeters,
4762
+ speedMps: point.speedMps,
4763
+ isStopAnchor: point.isStopAnchor
4764
+ }))
4765
+ : [];
4766
+ const resolveEndpoint = (kind) => {
4767
+ const fromPoints = kind === "start"
4768
+ ? tripPositions[0] ?? null
4769
+ : tripPositions[tripPositions.length - 1] ?? null;
4770
+ if (fromPoints) {
4771
+ return {
4772
+ ...fromPoints,
4773
+ label: kind === "start" ? "Start position" : "End position"
4774
+ };
4775
+ }
4776
+ if (segment.kind === "trip") {
4777
+ const place = kind === "start" ? segment.trip?.startPlace : segment.trip?.endPlace;
4778
+ if (place) {
4779
+ return {
4780
+ latitude: place.latitude,
4781
+ longitude: place.longitude,
4782
+ recordedAt: kind === "start" ? segment.startedAt : segment.endedAt,
4783
+ label: place.label
4784
+ };
4785
+ }
4786
+ }
4787
+ return null;
4788
+ };
4789
+ const totalMovingSeconds = rawTrips.length > 0
4790
+ ? rawTrips.reduce((sum, trip) => sum + trip.movingSeconds, 0)
4791
+ : segment.trip?.movingSeconds ?? 0;
4792
+ const totalDistanceMeters = rawTrips.length > 0
4793
+ ? rawTrips.reduce((sum, trip) => sum + trip.distanceMeters, 0)
4794
+ : segment.trip?.distanceMeters ?? 0;
4795
+ const tripDetail = segment.kind === "trip"
4796
+ ? {
4797
+ positions: tripPositions,
4798
+ startPosition: resolveEndpoint("start"),
4799
+ endPosition: resolveEndpoint("end"),
4800
+ totalDistanceMeters,
4801
+ movingSeconds: totalMovingSeconds,
4802
+ idleSeconds: rawTrips.length > 0
4803
+ ? rawTrips.reduce((sum, trip) => sum + trip.idleSeconds, 0)
4804
+ : segment.trip?.idleSeconds ?? 0,
4805
+ averageSpeedMps: totalMovingSeconds > 0
4806
+ ? round(totalDistanceMeters / totalMovingSeconds, 2)
4807
+ : segment.trip?.averageSpeedMps ?? null,
4808
+ maxSpeedMps: rawTrips.length > 0
4809
+ ? rawTrips.reduce((maxSpeed, trip) => Math.max(maxSpeed, trip.maxSpeedMps ?? 0), 0) || null
4810
+ : segment.trip?.maxSpeedMps ?? null,
4811
+ stopCount: rawTrips.length > 0
4812
+ ? rawTrips.reduce((sum, trip) => sum + trip.stops.length, 0)
4813
+ : segment.trip?.stops.length ?? 0
4814
+ }
4815
+ : null;
4816
+ return {
4817
+ segment,
4818
+ rawStays,
4819
+ rawTrips,
4820
+ stayDetail,
4821
+ tripDetail
4822
+ };
4823
+ }
4673
4824
  export function getMovementSelectionAggregate(input) {
4674
4825
  const parsed = movementSelectionAggregateSchema.parse(input);
4675
4826
  const placeRows = listMovementPlaceRows(parsed.userIds);
@@ -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",