forge-openclaw-plugin 0.2.29 → 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/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/index.html +2 -2
- package/dist/server/server/src/app.js +26 -5
- package/dist/server/server/src/movement.js +151 -0
- package/dist/server/server/src/services/life-force.js +84 -52
- package/dist/server/src/lib/api.js +12 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/forge-openclaw/SKILL.md +19 -2
- package/dist/assets/index-C6PCeHD_.css +0 -1
- package/dist/assets/index-bfHIqj0-.js +0 -85
- package/dist/assets/index-bfHIqj0-.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-
|
|
16
|
+
<script type="module" crossorigin src="/forge/assets/index-CPC6E84V.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-
|
|
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
|
|
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":"
|
|
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,
|
|
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
|
|
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
|
-
!
|
|
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
|
|
1823
|
-
|
|
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
|
-
|
|
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
|
|
1861
|
-
|
|
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
|
-
!
|
|
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(
|
|
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
|
-
!
|
|
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
|
-
|
|
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,
|
|
@@ -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) {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -403,8 +403,18 @@ Use the calendar tools when the request is about planning or availability rather
|
|
|
403
403
|
- `forge_connect_calendar_provider` only when the operator explicitly wants a new Google, Apple, Exchange Online, or custom CalDAV connection and the discovery choices are already known
|
|
404
404
|
- `forge_sync_calendar_connection` after a provider connection is created or when the calendar needs a fresh pull/push cycle
|
|
405
405
|
- `forge_create_work_block_template` as a convenience helper for Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom recurring blocks
|
|
406
|
-
- `forge_recommend_task_timeboxes` to find future slots that satisfy current rules
|
|
407
|
-
- `forge_create_task_timebox` as
|
|
406
|
+
- `forge_recommend_task_timeboxes` to find future slots that satisfy current rules when the user wants suggestions or when the agent needs help narrowing options
|
|
407
|
+
- `forge_create_task_timebox` as the main direct route for manual timeboxing once the slot is known; use it after reasoning from the live calendar overview or after accepting a suggested slot
|
|
408
|
+
|
|
409
|
+
Timebox planning rules for agents:
|
|
410
|
+
|
|
411
|
+
- prefer manual timeboxing when the agent already has enough calendar context to choose the slot itself
|
|
412
|
+
- use `forge_get_calendar_overview` first when the current day or week matters; reason over mirrored events, work blocks, existing timeboxes, and availability before placing the block
|
|
413
|
+
- use `forge_create_task_timebox` directly for the manual path with explicit `startsAt` and `endsAt`
|
|
414
|
+
- use `forge_recommend_task_timeboxes` only for the assisted path, then confirm one returned slot with `forge_create_task_timebox`
|
|
415
|
+
- when manually timeboxing, keep the title specific and task-shaped, not generic
|
|
416
|
+
- if the block needs a special Action Point profile, pass `activityPresetKey` and/or `customSustainRateApPerHour`
|
|
417
|
+
- if the user wants a note about why this slot exists, pass `overrideReason`
|
|
408
418
|
|
|
409
419
|
Use the health tools when the request is about sleep or sports review:
|
|
410
420
|
|
|
@@ -457,6 +467,13 @@ Use these exact calendar batch payload shapes when working generically:
|
|
|
457
467
|
- create a planned task slot:
|
|
458
468
|
`{"operations":[{"entityType":"task_timebox","data":{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T06:00:00.000Z","endsAt":"2026-04-03T07:30:00.000Z","source":"suggested"}}]}`
|
|
459
469
|
|
|
470
|
+
Use these exact timebox helper payload shapes when the dedicated helper is simpler than batch CRUD:
|
|
471
|
+
|
|
472
|
+
- create a manual task timebox directly:
|
|
473
|
+
`{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T06:00:00.000Z","endsAt":"2026-04-03T07:30:00.000Z","source":"manual","overrideReason":"Protected writing block before clinic.","activityPresetKey":"deep_work","customSustainRateApPerHour":6.5}`
|
|
474
|
+
- create a suggested task timebox after reviewing recommendations:
|
|
475
|
+
`{"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"}`
|
|
476
|
+
|
|
460
477
|
Use these interaction rules.
|
|
461
478
|
|
|
462
479
|
Keep the main discussion natural. Do not turn every conversation into a form. Do not offer Forge for every passing mention. Offer it once, near the end, only when the signal is strong and storing would help.
|