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.
- 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-CPC6E84V.js +85 -0
- package/dist/assets/index-CPC6E84V.js.map +1 -0
- package/dist/assets/index-DiyKCDxL.css +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 +113 -17
- package/dist/server/server/src/movement.js +151 -0
- 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/life-force.js +84 -52
- 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 +51 -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 +40 -7
- 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-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-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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":"
|
|
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
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
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
|
|
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",
|