forge-openclaw-plugin 0.2.29 → 0.2.32

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/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-bfHIqj0-.js"></script>
16
+ <script type="module" crossorigin src="/forge/assets/index-BE_4LX2c.js"></script>
17
17
  <link rel="modulepreload" crossorigin href="/forge/assets/vendor-OwcH20PM.js">
18
18
  <link rel="modulepreload" crossorigin href="/forge/assets/board-q8cfwaAW.js">
19
19
  <link rel="modulepreload" crossorigin href="/forge/assets/ui-BV0OYxkH.js">
20
20
  <link rel="modulepreload" crossorigin href="/forge/assets/motion-DHfqFntt.js">
21
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-C6PCeHD_.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>
@@ -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";
@@ -3668,7 +3668,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
3668
3668
  {
3669
3669
  toolName: "forge_recommend_task_timeboxes",
3670
3670
  summary: "Suggest future task slots that fit the current calendar rules and schedule.",
3671
- 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.",
3672
3672
  inputShape: "{ taskId: string, from?: string, to?: string, limit?: integer }",
3673
3673
  requiredFields: ["taskId"],
3674
3674
  notes: [
@@ -3680,15 +3680,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
3680
3680
  {
3681
3681
  toolName: "forge_create_task_timebox",
3682
3682
  summary: "Create a planned task timebox in the Forge calendar domain.",
3683
- whenToUse: "Use after choosing a valid future slot or when creating a manual timebox directly.",
3684
- 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 }',
3685
3685
  requiredFields: ["taskId", "title", "startsAt", "endsAt"],
3686
3686
  notes: [
3687
+ "Manual timeboxing is the main direct path when the agent already understands the calendar and wants to choose the slot itself.",
3687
3688
  "Forge publishes these through the shared Forge write target during provider sync.",
3688
3689
  "Live task runs can later attach to matching timeboxes.",
3689
3690
  "This is a convenience helper; agents can also create task_timebox through forge_create_entities."
3690
3691
  ],
3691
- 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}'
3692
3693
  },
3693
3694
  {
3694
3695
  toolName: "forge_grant_reward_bonus",
@@ -5580,6 +5581,15 @@ export async function buildServer(options = {}) {
5580
5581
  }
5581
5582
  return { movement };
5582
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
+ });
5583
5593
  app.post("/api/v1/movement/selection", async (request) => ({
5584
5594
  movement: getMovementSelectionAggregate(movementSelectionAggregateSchema.parse(request.body ?? {}))
5585
5595
  }));
@@ -5674,6 +5684,17 @@ export async function buildServer(options = {}) {
5674
5684
  })
5675
5685
  };
5676
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
+ });
5677
5698
  app.post("/api/v1/mobile/movement/user-boxes", async (request, reply) => {
5678
5699
  const parsed = movementMobileUserBoxCreateSchema.parse(request.body ?? {});
5679
5700
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
@@ -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);
@@ -234,6 +234,15 @@ function computeUncoveredSeconds(window, blockingWindows) {
234
234
  coveredMs += activeEndMs - activeStartMs;
235
235
  return Math.max(0, totalSeconds - Math.floor(coveredMs / 1000));
236
236
  }
237
+ function containsInstant(window, instantIso) {
238
+ const instantMs = Date.parse(instantIso);
239
+ return (Number.isFinite(instantMs) &&
240
+ Date.parse(window.startAt) <= instantMs &&
241
+ Date.parse(window.endAt) > instantMs);
242
+ }
243
+ function isInstantCovered(instantIso, blockingWindows) {
244
+ return blockingWindows.some((window) => containsInstant(window, instantIso));
245
+ }
237
246
  function nowIso() {
238
247
  return new Date().toISOString();
239
248
  }
@@ -1635,6 +1644,36 @@ function readTaskTimeboxLifeForceRows(userId, range) {
1635
1644
  return [];
1636
1645
  }
1637
1646
  }
1647
+ function readCalendarEventLifeForceRows(range) {
1648
+ try {
1649
+ return getDatabase()
1650
+ .prepare(`SELECT
1651
+ forge_events.id,
1652
+ forge_events.title,
1653
+ forge_events.start_at,
1654
+ forge_events.end_at,
1655
+ forge_events.availability,
1656
+ forge_events.event_type,
1657
+ COUNT(forge_event_links.id) AS link_count
1658
+ FROM forge_events
1659
+ LEFT JOIN forge_event_links
1660
+ ON forge_event_links.forge_event_id = forge_events.id
1661
+ WHERE forge_events.deleted_at IS NULL
1662
+ AND forge_events.end_at > ?
1663
+ AND forge_events.start_at < ?
1664
+ GROUP BY
1665
+ forge_events.id,
1666
+ forge_events.title,
1667
+ forge_events.start_at,
1668
+ forge_events.end_at,
1669
+ forge_events.availability,
1670
+ forge_events.event_type`)
1671
+ .all(range.from, range.to);
1672
+ }
1673
+ catch {
1674
+ return [];
1675
+ }
1676
+ }
1638
1677
  function readWorkBlockTemplateLifeForceRows(userId) {
1639
1678
  try {
1640
1679
  return getDatabase()
@@ -1717,7 +1756,7 @@ function buildWorkBlockProfile(input) {
1717
1756
  endMinute
1718
1757
  });
1719
1758
  }
1720
- function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, activeTaskRunTaskIds, actualSourceWindows) {
1759
+ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, activeTaskRunTaskIds, actualSourceWindows, calendarEventWindows) {
1721
1760
  const actualContributions = [];
1722
1761
  const plannedDrains = [];
1723
1762
  const activeDrains = [];
@@ -1756,11 +1795,12 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1756
1795
  startsAt: row.starts_at,
1757
1796
  endsAt: row.ends_at
1758
1797
  });
1798
+ const higherPriorityWindows = [...actualSourceWindows, ...calendarEventWindows];
1759
1799
  const elapsedWindow = {
1760
1800
  startAt: row.starts_at,
1761
1801
  endAt: new Date(Math.min(now.getTime(), Date.parse(row.ends_at))).toISOString()
1762
1802
  };
1763
- const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, actualSourceWindows);
1803
+ const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, higherPriorityWindows);
1764
1804
  if (elapsedSeconds > 0) {
1765
1805
  actualContributions.push({
1766
1806
  entityType: "task_timebox",
@@ -1778,7 +1818,15 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1778
1818
  }
1779
1819
  const remainingStartMs = Math.max(now.getTime(), Date.parse(row.starts_at));
1780
1820
  const remainingEndMs = Math.min(range.endMs, Date.parse(row.ends_at));
1781
- const remainingSeconds = Math.max(0, Math.floor((remainingEndMs - remainingStartMs) / 1000));
1821
+ const remainingWindow = remainingEndMs > remainingStartMs
1822
+ ? {
1823
+ startAt: new Date(remainingStartMs).toISOString(),
1824
+ endAt: new Date(remainingEndMs).toISOString()
1825
+ }
1826
+ : null;
1827
+ const remainingSeconds = remainingWindow
1828
+ ? computeUncoveredSeconds(remainingWindow, higherPriorityWindows)
1829
+ : 0;
1782
1830
  if (remainingSeconds > 0) {
1783
1831
  plannedDrains.push({
1784
1832
  entityType: "task_timebox",
@@ -1789,15 +1837,15 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1789
1837
  rateApPerHour: profile.sustainRateApPerHour,
1790
1838
  title: row.title,
1791
1839
  why: "Planned task timeboxes forecast how much Action Point throughput is still booked today.",
1792
- startsAt: row.starts_at,
1793
- endsAt: row.ends_at,
1840
+ startsAt: remainingWindow?.startAt ?? row.starts_at,
1841
+ endsAt: remainingWindow?.endAt ?? row.ends_at,
1794
1842
  role: "secondary"
1795
1843
  });
1796
1844
  }
1797
1845
  if (Date.parse(row.starts_at) <= now.getTime() &&
1798
1846
  Date.parse(row.ends_at) > now.getTime() &&
1799
1847
  !activeTaskRunTaskIds.has(row.task_id) &&
1800
- !actualSourceWindows.some((window) => overlapsWindow(row.starts_at, row.ends_at, window.startAt, window.endAt))) {
1848
+ !isInstantCovered(now.toISOString(), higherPriorityWindows)) {
1801
1849
  activeDrains.push({
1802
1850
  entityType: "task_timebox",
1803
1851
  entityId: row.id,
@@ -1819,8 +1867,11 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1819
1867
  startAt: block.start_at,
1820
1868
  endAt: block.end_at
1821
1869
  });
1822
- const overlapsTimebox = timeboxes.some((timebox) => timebox.status !== "cancelled" &&
1823
- overlapsWindow(block.start_at, block.end_at, timebox.starts_at, timebox.ends_at));
1870
+ const higherPriorityWindows = [
1871
+ ...actualSourceWindows,
1872
+ ...calendarEventWindows,
1873
+ ...timeboxWindows
1874
+ ];
1824
1875
  const profile = buildEffectiveProfile(buildWorkBlockProfile({
1825
1876
  templateId: block.id,
1826
1877
  title: block.title,
@@ -1832,11 +1883,8 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1832
1883
  startAt: block.start_at,
1833
1884
  endAt: new Date(Math.min(now.getTime(), Date.parse(block.end_at))).toISOString()
1834
1885
  };
1835
- const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, [
1836
- ...actualSourceWindows,
1837
- ...timeboxWindows
1838
- ]);
1839
- if (elapsedSeconds > 0 && !overlapsTimebox) {
1886
+ const elapsedSeconds = computeUncoveredSeconds(elapsedWindow, higherPriorityWindows);
1887
+ if (elapsedSeconds > 0) {
1840
1888
  actualContributions.push({
1841
1889
  entityType: "work_block",
1842
1890
  entityId: block.instance_id,
@@ -1857,8 +1905,16 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1857
1905
  }
1858
1906
  const remainingStartMs = Math.max(now.getTime(), Date.parse(block.start_at));
1859
1907
  const remainingEndMs = Math.min(range.endMs, Date.parse(block.end_at));
1860
- const remainingSeconds = Math.max(0, Math.floor((remainingEndMs - remainingStartMs) / 1000));
1861
- if (remainingSeconds > 0 && !overlapsTimebox) {
1908
+ const remainingWindow = remainingEndMs > remainingStartMs
1909
+ ? {
1910
+ startAt: new Date(remainingStartMs).toISOString(),
1911
+ endAt: new Date(remainingEndMs).toISOString()
1912
+ }
1913
+ : null;
1914
+ const remainingSeconds = remainingWindow
1915
+ ? computeUncoveredSeconds(remainingWindow, higherPriorityWindows)
1916
+ : 0;
1917
+ if (remainingSeconds > 0) {
1862
1918
  plannedDrains.push({
1863
1919
  entityType: "work_block",
1864
1920
  entityId: block.instance_id,
@@ -1868,8 +1924,8 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1868
1924
  rateApPerHour: profile.sustainRateApPerHour,
1869
1925
  title: block.title,
1870
1926
  why: "Work blocks act as planning containers and forecast background load when no richer task plan exists.",
1871
- startsAt: block.start_at,
1872
- endsAt: block.end_at,
1927
+ startsAt: remainingWindow?.startAt ?? block.start_at,
1928
+ endsAt: remainingWindow?.endAt ?? block.end_at,
1873
1929
  role: "background",
1874
1930
  metadata: {
1875
1931
  templateId: block.id,
@@ -1879,9 +1935,8 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1879
1935
  }
1880
1936
  if (Date.parse(block.start_at) <= now.getTime() &&
1881
1937
  Date.parse(block.end_at) > now.getTime() &&
1882
- !overlapsTimebox &&
1883
1938
  activeTaskRunTaskIds.size === 0 &&
1884
- !actualSourceWindows.some((window) => overlapsWindow(block.start_at, block.end_at, window.startAt, window.endAt))) {
1939
+ !isInstantCovered(now.toISOString(), higherPriorityWindows)) {
1885
1940
  activeDrains.push({
1886
1941
  entityType: "work_block",
1887
1942
  entityId: block.instance_id,
@@ -1909,35 +1964,12 @@ function buildTimeboxAndWorkBlockDrains(userId, range, now, lifeForceProfile, ac
1909
1964
  workBlockWindows
1910
1965
  };
1911
1966
  }
1912
- function buildCalendarDrains(userId, now, range, lifeForceProfile, blockingWindows) {
1967
+ function buildCalendarDrains(rows, now, range, lifeForceProfile, blockingWindows) {
1913
1968
  const actualContributions = [];
1914
1969
  const nowIsoValue = now.toISOString();
1915
1970
  const activeDrains = [];
1916
1971
  const plannedDrains = [];
1917
1972
  try {
1918
- const rows = getDatabase()
1919
- .prepare(`SELECT
1920
- forge_events.id,
1921
- forge_events.title,
1922
- forge_events.start_at,
1923
- forge_events.end_at,
1924
- forge_events.availability,
1925
- forge_events.event_type,
1926
- COUNT(forge_event_links.id) AS link_count
1927
- FROM forge_events
1928
- LEFT JOIN forge_event_links
1929
- ON forge_event_links.forge_event_id = forge_events.id
1930
- WHERE forge_events.deleted_at IS NULL
1931
- AND forge_events.end_at > ?
1932
- AND forge_events.start_at < ?
1933
- GROUP BY
1934
- forge_events.id,
1935
- forge_events.title,
1936
- forge_events.start_at,
1937
- forge_events.end_at,
1938
- forge_events.availability,
1939
- forge_events.event_type`)
1940
- .all(range.from, range.to);
1941
1973
  for (const row of rows) {
1942
1974
  const calendarProfile = buildEffectiveProfile(readEntityActionProfile("calendar_event", row.id, {
1943
1975
  profileKey: `calendar_event_${row.id}`,
@@ -1952,7 +1984,6 @@ function buildCalendarDrains(userId, now, range, lifeForceProfile, blockingWindo
1952
1984
  startAt: row.start_at,
1953
1985
  endAt: row.end_at
1954
1986
  }), lifeForceProfile);
1955
- const overlapsBlockingWindow = blockingWindows.some((window) => overlapsWindow(row.start_at, row.end_at, window.startAt, window.endAt));
1956
1987
  const elapsedWindow = {
1957
1988
  startAt: row.start_at,
1958
1989
  endAt: new Date(Math.min(now.getTime(), Date.parse(row.end_at))).toISOString()
@@ -1993,7 +2024,7 @@ function buildCalendarDrains(userId, now, range, lifeForceProfile, blockingWindo
1993
2024
  }
1994
2025
  if (row.start_at <= nowIsoValue &&
1995
2026
  row.end_at > nowIsoValue &&
1996
- !overlapsBlockingWindow) {
2027
+ !isInstantCovered(nowIsoValue, blockingWindows)) {
1997
2028
  activeDrains.push({
1998
2029
  entityType: "calendar_event",
1999
2030
  entityId: row.id,
@@ -2172,14 +2203,15 @@ export function buildLifeForcePayload(now = new Date(), userIds) {
2172
2203
  startAt: entry.startsAt,
2173
2204
  endAt: entry.endsAt
2174
2205
  }));
2206
+ const calendarRows = readCalendarEventLifeForceRows(range);
2207
+ const calendarEventWindows = calendarRows.map((row) => ({
2208
+ startAt: row.start_at,
2209
+ endAt: row.end_at
2210
+ }));
2175
2211
  const activeTaskRunTaskIds = new Set(taskRuns.activeDrains.map((entry) => entry.entityId));
2176
- const plannedContainers = buildTimeboxAndWorkBlockDrains(user.id, range, now, profile, activeTaskRunTaskIds, actualSourceWindows);
2177
- const calendarBlockingWindows = [
2178
- ...actualSourceWindows,
2179
- ...plannedContainers.timeboxWindows,
2180
- ...plannedContainers.workBlockWindows
2181
- ];
2182
- const calendarDrains = buildCalendarDrains(user.id, now, range, profile, calendarBlockingWindows);
2212
+ const plannedContainers = buildTimeboxAndWorkBlockDrains(user.id, range, now, profile, activeTaskRunTaskIds, actualSourceWindows, calendarEventWindows);
2213
+ const calendarBlockingWindows = [...actualSourceWindows];
2214
+ const calendarDrains = buildCalendarDrains(calendarRows, now, range, profile, calendarBlockingWindows);
2183
2215
  const contributions = [
2184
2216
  ...taskRuns.contributions,
2185
2217
  ...adjustments,
@@ -1,3 +1,5 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { request as httpsRequest } from "node:https";
1
3
  import { spawn } from "node:child_process";
2
4
  import { existsSync } from "node:fs";
3
5
  import { access, readFile } from "node:fs/promises";
@@ -133,9 +135,13 @@ function copyProxyHeaders(response, reply) {
133
135
  reply.header(name, value);
134
136
  }
135
137
  }
138
+ function buildDevWebTarget(origin, pathname, search) {
139
+ const target = new URL(pathname.startsWith("/") ? pathname.slice(1) : pathname, origin);
140
+ target.search = search;
141
+ return target;
142
+ }
136
143
  async function proxyDevAsset(input) {
137
- const target = new URL(input.pathname.startsWith("/") ? input.pathname.slice(1) : input.pathname, input.origin);
138
- target.search = input.search;
144
+ const target = buildDevWebTarget(input.origin, input.pathname, input.search);
139
145
  const response = await input.fetchImpl(target, { redirect: "manual" });
140
146
  input.reply.code(response.status);
141
147
  copyProxyHeaders(response, input.reply);
@@ -147,6 +153,56 @@ async function proxyDevAsset(input) {
147
153
  }
148
154
  return Buffer.from(await response.arrayBuffer());
149
155
  }
156
+ function writeProxyUpgradeResponse(socket, response) {
157
+ const statusCode = response.statusCode ?? 101;
158
+ const statusMessage = response.statusMessage ?? "Switching Protocols";
159
+ const headerLines = [];
160
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
161
+ const name = response.rawHeaders[index];
162
+ const value = response.rawHeaders[index + 1];
163
+ if (name && value) {
164
+ headerLines.push(`${name}: ${value}`);
165
+ }
166
+ }
167
+ socket.write(`HTTP/${response.httpVersion} ${statusCode} ${statusMessage}\r\n${headerLines.join("\r\n")}\r\n\r\n`);
168
+ }
169
+ async function proxyDevWebSocket(input) {
170
+ const requestTarget = parseRequestTarget(input.request.url ?? "/");
171
+ const normalizedRequestPath = stripBasePath(requestTarget.pathname, getDefaultBasePath());
172
+ if (!normalizedRequestPath.startsWith("/__vite_hmr")) {
173
+ return false;
174
+ }
175
+ const devWebOrigin = await input.devWebRuntime.ensureReady();
176
+ if (!devWebOrigin) {
177
+ input.socket.destroy();
178
+ return true;
179
+ }
180
+ const target = buildDevWebTarget(devWebOrigin, normalizedRequestPath, requestTarget.search);
181
+ const proxyRequest = (target.protocol === "https:" ? httpsRequest : httpRequest)(target, {
182
+ headers: {
183
+ ...input.request.headers,
184
+ host: target.host
185
+ }
186
+ });
187
+ proxyRequest.on("upgrade", (response, proxySocket, proxyHead) => {
188
+ writeProxyUpgradeResponse(input.socket, response);
189
+ if (proxyHead.length > 0) {
190
+ input.socket.write(proxyHead);
191
+ }
192
+ if (input.head.length > 0) {
193
+ proxySocket.write(input.head);
194
+ }
195
+ proxySocket.pipe(input.socket).pipe(proxySocket);
196
+ });
197
+ proxyRequest.on("response", () => {
198
+ input.socket.destroy();
199
+ });
200
+ proxyRequest.on("error", () => {
201
+ input.socket.destroy();
202
+ });
203
+ proxyRequest.end();
204
+ return true;
205
+ }
150
206
  async function waitForProcessExit(child, timeoutMs = 5_000) {
151
207
  if (child.exitCode !== null) {
152
208
  return;
@@ -327,6 +383,16 @@ export async function registerWebRoutes(app, options = {}) {
327
383
  app.addHook("onClose", async () => {
328
384
  await devWebRuntime.stop();
329
385
  });
386
+ app.server.on("upgrade", (request, socket, head) => {
387
+ void (async () => {
388
+ await proxyDevWebSocket({
389
+ devWebRuntime,
390
+ request,
391
+ socket,
392
+ head
393
+ });
394
+ })();
395
+ });
330
396
  app.get("/", async (_request, reply) => serveAsset("/", reply, { devWebRuntime, fetchImpl }));
331
397
  app.get("/*", async (request, reply) => serveAsset(request.url, reply, { devWebRuntime, fetchImpl }));
332
398
  }
@@ -1657,9 +1657,21 @@ export function patchMovementPlace(placeId, patch) {
1657
1657
  body: JSON.stringify(patch)
1658
1658
  });
1659
1659
  }
1660
+ export function patchMovementStay(stayId, patch) {
1661
+ return request(`/api/v1/movement/stays/${stayId}`, {
1662
+ method: "PATCH",
1663
+ body: JSON.stringify(patch)
1664
+ });
1665
+ }
1660
1666
  export function getMovementTripDetail(tripId) {
1661
1667
  return request(`/api/v1/movement/trips/${tripId}`);
1662
1668
  }
1669
+ export function getMovementBoxDetail(boxId, userIds) {
1670
+ const search = new URLSearchParams();
1671
+ appendUserIds(search, coerceUserIds(userIds));
1672
+ const suffix = search.size > 0 ? `?${search.toString()}` : "";
1673
+ return request(`/api/v1/movement/boxes/${boxId}${suffix}`);
1674
+ }
1663
1675
  export function getMovementTimeline(input) {
1664
1676
  const search = new URLSearchParams();
1665
1677
  if (input?.before) {
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.29",
5
+ "version": "0.2.32",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.29",
3
+ "version": "0.2.32",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",