forge-openclaw-plugin 0.2.27 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/assets/{board-C6jCchjI.js → board-q8cfwaAW.js} +2 -2
- package/dist/assets/{board-C6jCchjI.js.map → board-q8cfwaAW.js.map} +1 -1
- package/dist/assets/index-C6PCeHD_.css +1 -0
- package/dist/assets/index-bfHIqj0-.js +85 -0
- package/dist/assets/index-bfHIqj0-.js.map +1 -0
- package/dist/assets/{motion-DFHrH2rd.js → motion-DHfqFntt.js} +2 -2
- package/dist/assets/{motion-DFHrH2rd.js.map → motion-DHfqFntt.js.map} +1 -1
- package/dist/assets/{table-ZL7Di_u3.js → table-DLweENXt.js} +2 -2
- package/dist/assets/{table-ZL7Di_u3.js.map → table-DLweENXt.js.map} +1 -1
- package/dist/assets/{ui-CKNPpz7q.js → ui-BV0OYxkH.js} +2 -2
- package/dist/assets/{ui-CKNPpz7q.js.map → ui-BV0OYxkH.js.map} +1 -1
- package/dist/assets/{vendor-DoNZuFhn.js → vendor-OwcH20PM.js} +204 -204
- package/dist/assets/vendor-OwcH20PM.js.map +1 -0
- 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 +331 -14
- package/dist/server/server/src/openapi.js +828 -3
- package/dist/server/server/src/repositories/calendar.js +295 -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-model.js +20 -0
- package/dist/server/server/src/services/life-force.js +1333 -97
- package/dist/server/server/src/services/macos-calendar-helper.js +748 -0
- package/dist/server/server/src/types.js +67 -3
- package/dist/server/src/lib/api-error.js +2 -0
- package/dist/server/src/lib/api.js +39 -2
- package/dist/server/src/lib/calendar-name-deduper.js +2 -0
- package/dist/server/src/lib/snapshot-normalizer.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 +38 -5
- package/skills/forge-openclaw/entity_conversation_playbooks.md +326 -5
- package/skills/forge-openclaw/psyche_entity_playbooks.md +57 -0
- package/dist/assets/index-DVvS8iiU.css +0 -1
- package/dist/assets/index-zYB-9Dfo.js +0 -85
- package/dist/assets/index-zYB-9Dfo.js.map +0 -1
- package/dist/assets/vendor-DoNZuFhn.js.map +0 -1
|
@@ -4,6 +4,7 @@ import { recordActivityEvent } from "./activity-events.js";
|
|
|
4
4
|
import { decorateOwnedEntity, filterOwnedEntities, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
|
|
5
5
|
import { getProjectById } from "./projects.js";
|
|
6
6
|
import { getTaskById } from "./tasks.js";
|
|
7
|
+
import { buildCalendarEventActionProfile, buildTaskTimeboxActionProfile, buildWorkBlockTemplateActionProfile, readEntityActionProfile, upsertEntityActionProfile } from "../services/life-force.js";
|
|
7
8
|
import { calendarConnectionSchema, calendarContextConflictSchema, calendarEventSchema, calendarEventLinkSchema, calendarEventSourceSchema, calendarOverviewPayloadSchema, calendarSchema, calendarSchedulingRulesSchema, taskTimeboxSchema, workBlockInstanceSchema, workBlockTemplateSchema } from "../types.js";
|
|
8
9
|
const DEFAULT_SCHEDULING_RULES = {
|
|
9
10
|
allowWorkBlockKinds: [],
|
|
@@ -63,6 +64,12 @@ function mapCalendar(row) {
|
|
|
63
64
|
canWrite: Boolean(row.can_write),
|
|
64
65
|
selectedForSync: Boolean(row.selected_for_sync),
|
|
65
66
|
forgeManaged: Boolean(row.forge_managed),
|
|
67
|
+
sourceId: row.source_id,
|
|
68
|
+
sourceTitle: row.source_title,
|
|
69
|
+
sourceType: row.source_type,
|
|
70
|
+
calendarType: row.calendar_type,
|
|
71
|
+
hostCalendarId: row.host_calendar_id,
|
|
72
|
+
canonicalKey: row.canonical_key,
|
|
66
73
|
lastSyncedAt: row.last_synced_at,
|
|
67
74
|
createdAt: row.created_at,
|
|
68
75
|
updatedAt: row.updated_at
|
|
@@ -149,6 +156,11 @@ function mapEvent(row) {
|
|
|
149
156
|
categories: JSON.parse(row.categories_json || "[]"),
|
|
150
157
|
sourceMappings,
|
|
151
158
|
links: listEventLinksForEvent(row.id),
|
|
159
|
+
actionProfile: readEntityActionProfile("calendar_event", row.id, {
|
|
160
|
+
profileKey: `calendar_event_${row.id}`,
|
|
161
|
+
title: row.title,
|
|
162
|
+
entityType: "calendar_event"
|
|
163
|
+
}),
|
|
152
164
|
remoteUpdatedAt: primarySource?.lastSyncedAt ?? null,
|
|
153
165
|
deletedAt: row.deleted_at,
|
|
154
166
|
createdAt: row.created_at,
|
|
@@ -168,11 +180,17 @@ function mapWorkBlockTemplate(row) {
|
|
|
168
180
|
startsOn: row.starts_on,
|
|
169
181
|
endsOn: row.ends_on,
|
|
170
182
|
blockingState: row.blocking_state,
|
|
183
|
+
actionProfile: readEntityActionProfile("work_block_template", row.id, {
|
|
184
|
+
profileKey: `work_block_template_${row.id}`,
|
|
185
|
+
title: row.title,
|
|
186
|
+
entityType: "work_block_template"
|
|
187
|
+
}),
|
|
171
188
|
createdAt: row.created_at,
|
|
172
189
|
updatedAt: row.updated_at
|
|
173
190
|
});
|
|
174
191
|
}
|
|
175
192
|
function mapTimebox(row) {
|
|
193
|
+
const task = getTaskById(row.task_id);
|
|
176
194
|
return taskTimeboxSchema.parse({
|
|
177
195
|
id: row.id,
|
|
178
196
|
taskId: row.task_id,
|
|
@@ -187,6 +205,21 @@ function mapTimebox(row) {
|
|
|
187
205
|
startsAt: row.starts_at,
|
|
188
206
|
endsAt: row.ends_at,
|
|
189
207
|
overrideReason: row.override_reason,
|
|
208
|
+
actionProfile: readEntityActionProfile("task_timebox", row.id, {
|
|
209
|
+
profileKey: `task_timebox_${row.id}`,
|
|
210
|
+
title: row.title,
|
|
211
|
+
entityType: "task_timebox"
|
|
212
|
+
}) ??
|
|
213
|
+
(task
|
|
214
|
+
? buildTaskTimeboxActionProfile({
|
|
215
|
+
timeboxId: row.id,
|
|
216
|
+
title: row.title,
|
|
217
|
+
taskId: row.task_id,
|
|
218
|
+
taskPlannedDurationSeconds: task.plannedDurationSeconds,
|
|
219
|
+
startsAt: row.starts_at,
|
|
220
|
+
endsAt: row.ends_at
|
|
221
|
+
})
|
|
222
|
+
: null),
|
|
190
223
|
createdAt: row.created_at,
|
|
191
224
|
updatedAt: row.updated_at
|
|
192
225
|
});
|
|
@@ -228,6 +261,13 @@ export function readEncryptedSecret(secretId) {
|
|
|
228
261
|
export function deleteEncryptedSecret(secretId) {
|
|
229
262
|
getDatabase().prepare(`DELETE FROM stored_secrets WHERE id = ?`).run(secretId);
|
|
230
263
|
}
|
|
264
|
+
export function isSupersededCalendarConnection(connectionId) {
|
|
265
|
+
const connection = getCalendarConnectionById(connectionId);
|
|
266
|
+
if (!connection) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
return isSupersededConnection(connection);
|
|
270
|
+
}
|
|
231
271
|
export function listCalendarConnections() {
|
|
232
272
|
const rows = getDatabase()
|
|
233
273
|
.prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
|
|
@@ -237,6 +277,15 @@ export function listCalendarConnections() {
|
|
|
237
277
|
.all();
|
|
238
278
|
return rows.map(mapConnection);
|
|
239
279
|
}
|
|
280
|
+
function isSupersededConnection(connection) {
|
|
281
|
+
return (typeof connection.config.replacedByConnectionId === "string" &&
|
|
282
|
+
connection.config.replacedByConnectionId.trim().length > 0);
|
|
283
|
+
}
|
|
284
|
+
function activeConnectionIds() {
|
|
285
|
+
return new Set(listCalendarConnections()
|
|
286
|
+
.filter((connection) => !isSupersededConnection(connection))
|
|
287
|
+
.map((connection) => connection.id));
|
|
288
|
+
}
|
|
240
289
|
export function getCalendarConnectionById(connectionId) {
|
|
241
290
|
const row = getDatabase()
|
|
242
291
|
.prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
|
|
@@ -303,6 +352,70 @@ export function deleteExternalEventsForConnection(connectionId) {
|
|
|
303
352
|
}
|
|
304
353
|
return rows.map((row) => row.id);
|
|
305
354
|
}
|
|
355
|
+
export function rehomeCalendarConnectionReferences(input) {
|
|
356
|
+
return runInTransaction(() => {
|
|
357
|
+
const fromCalendars = listCalendars(input.fromConnectionId, {
|
|
358
|
+
includeUnselected: true
|
|
359
|
+
});
|
|
360
|
+
const toCalendars = listCalendars(input.toConnectionId, {
|
|
361
|
+
includeUnselected: true
|
|
362
|
+
});
|
|
363
|
+
const toForgeCalendar = toCalendars.find((calendar) => calendar.forgeManaged) ??
|
|
364
|
+
toCalendars.find((calendar) => calendar.canWrite) ??
|
|
365
|
+
null;
|
|
366
|
+
const toByCanonicalKey = new Map(toCalendars
|
|
367
|
+
.filter((calendar) => typeof calendar.canonicalKey === "string" &&
|
|
368
|
+
calendar.canonicalKey.trim().length > 0)
|
|
369
|
+
.map((calendar) => [calendar.canonicalKey, calendar]));
|
|
370
|
+
const mappedCalendarIds = new Map();
|
|
371
|
+
for (const fromCalendar of fromCalendars) {
|
|
372
|
+
const mapped = (fromCalendar.canonicalKey
|
|
373
|
+
? toByCanonicalKey.get(fromCalendar.canonicalKey)
|
|
374
|
+
: null) ??
|
|
375
|
+
(fromCalendar.forgeManaged ? toForgeCalendar : null) ??
|
|
376
|
+
null;
|
|
377
|
+
mappedCalendarIds.set(fromCalendar.id, mapped?.id ?? null);
|
|
378
|
+
}
|
|
379
|
+
const now = nowIso();
|
|
380
|
+
const forgeEventRows = getDatabase()
|
|
381
|
+
.prepare(`SELECT id, preferred_calendar_id
|
|
382
|
+
FROM forge_events
|
|
383
|
+
WHERE ownership = 'forge' AND preferred_connection_id = ?`)
|
|
384
|
+
.all(input.fromConnectionId);
|
|
385
|
+
const updateForgeEvent = getDatabase().prepare(`UPDATE forge_events
|
|
386
|
+
SET preferred_connection_id = ?, preferred_calendar_id = ?, updated_at = ?
|
|
387
|
+
WHERE id = ?`);
|
|
388
|
+
for (const row of forgeEventRows) {
|
|
389
|
+
const nextCalendarId = row.preferred_calendar_id
|
|
390
|
+
? (mappedCalendarIds.get(row.preferred_calendar_id) ?? toForgeCalendar?.id ?? null)
|
|
391
|
+
: (toForgeCalendar?.id ?? null);
|
|
392
|
+
updateForgeEvent.run(nextCalendarId ? input.toConnectionId : null, nextCalendarId, now, row.id);
|
|
393
|
+
}
|
|
394
|
+
const timeboxRows = getDatabase()
|
|
395
|
+
.prepare(`SELECT id, calendar_id
|
|
396
|
+
FROM task_timeboxes
|
|
397
|
+
WHERE connection_id = ?`)
|
|
398
|
+
.all(input.fromConnectionId);
|
|
399
|
+
const updateTimebox = getDatabase().prepare(`UPDATE task_timeboxes
|
|
400
|
+
SET connection_id = ?, calendar_id = ?, remote_event_id = NULL, updated_at = ?
|
|
401
|
+
WHERE id = ?`);
|
|
402
|
+
for (const row of timeboxRows) {
|
|
403
|
+
const nextCalendarId = row.calendar_id
|
|
404
|
+
? (mappedCalendarIds.get(row.calendar_id) ?? toForgeCalendar?.id ?? null)
|
|
405
|
+
: (toForgeCalendar?.id ?? null);
|
|
406
|
+
updateTimebox.run(nextCalendarId ? input.toConnectionId : null, nextCalendarId, now, row.id);
|
|
407
|
+
}
|
|
408
|
+
getDatabase()
|
|
409
|
+
.prepare(`DELETE FROM forge_event_sources
|
|
410
|
+
WHERE connection_id = ?
|
|
411
|
+
AND forge_event_id IN (
|
|
412
|
+
SELECT id
|
|
413
|
+
FROM forge_events
|
|
414
|
+
WHERE ownership = 'forge'
|
|
415
|
+
)`)
|
|
416
|
+
.run(input.fromConnectionId);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
306
419
|
export function detachConnectionFromForgeEvents(connectionId) {
|
|
307
420
|
const now = nowIso();
|
|
308
421
|
getDatabase()
|
|
@@ -325,16 +438,23 @@ export function listCalendars(connectionId, options = {}) {
|
|
|
325
438
|
: "WHERE (selected_for_sync = 1 OR forge_managed = 1)";
|
|
326
439
|
const rows = getDatabase()
|
|
327
440
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
441
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
328
442
|
last_synced_at, created_at, updated_at
|
|
329
443
|
FROM calendar_calendars
|
|
330
444
|
${connectionId ? `WHERE connection_id = ? ${visibilityClause}` : visibilityClause}
|
|
331
445
|
ORDER BY forge_managed DESC, title ASC`)
|
|
332
446
|
.all(...(connectionId ? [connectionId] : []));
|
|
333
|
-
|
|
447
|
+
const mapped = rows.map(mapCalendar);
|
|
448
|
+
if (connectionId) {
|
|
449
|
+
return mapped;
|
|
450
|
+
}
|
|
451
|
+
const activeIds = activeConnectionIds();
|
|
452
|
+
return mapped.filter((calendar) => activeIds.has(calendar.connectionId));
|
|
334
453
|
}
|
|
335
454
|
export function getCalendarById(calendarId) {
|
|
336
455
|
const row = getDatabase()
|
|
337
456
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
457
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
338
458
|
last_synced_at, created_at, updated_at
|
|
339
459
|
FROM calendar_calendars
|
|
340
460
|
WHERE id = ?`)
|
|
@@ -344,6 +464,7 @@ export function getCalendarById(calendarId) {
|
|
|
344
464
|
function getDefaultWritableCalendar() {
|
|
345
465
|
const row = getDatabase()
|
|
346
466
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
467
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
347
468
|
last_synced_at, created_at, updated_at
|
|
348
469
|
FROM calendar_calendars
|
|
349
470
|
WHERE can_write = 1
|
|
@@ -356,6 +477,7 @@ function getDefaultWritableCalendar() {
|
|
|
356
477
|
export function getCalendarByRemoteId(connectionId, remoteId) {
|
|
357
478
|
const row = getDatabase()
|
|
358
479
|
.prepare(`SELECT id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
480
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key,
|
|
359
481
|
last_synced_at, created_at, updated_at
|
|
360
482
|
FROM calendar_calendars
|
|
361
483
|
WHERE connection_id = ? AND remote_id = ?`)
|
|
@@ -368,18 +490,21 @@ export function upsertCalendarRecord(connectionId, input) {
|
|
|
368
490
|
if (existing) {
|
|
369
491
|
getDatabase()
|
|
370
492
|
.prepare(`UPDATE calendar_calendars
|
|
371
|
-
SET title = ?, description = ?, color = ?, timezone = ?, is_primary = ?, can_write = ?, selected_for_sync = ?, forge_managed = ?,
|
|
493
|
+
SET title = ?, description = ?, color = ?, timezone = ?, is_primary = ?, can_write = ?, selected_for_sync = ?, forge_managed = ?,
|
|
494
|
+
source_id = ?, source_title = ?, source_type = ?, calendar_type = ?, host_calendar_id = ?, canonical_key = ?,
|
|
495
|
+
last_synced_at = ?, updated_at = ?
|
|
372
496
|
WHERE id = ?`)
|
|
373
|
-
.run(input.title, input.description ?? existing.description, input.color ?? existing.color, normalizeTimezone(input.timezone ?? existing.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, now, now, existing.id);
|
|
497
|
+
.run(input.title, input.description ?? existing.description, input.color ?? existing.color, normalizeTimezone(input.timezone ?? existing.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, input.sourceId ?? existing.sourceId, input.sourceTitle ?? existing.sourceTitle, input.sourceType ?? existing.sourceType, input.calendarType ?? existing.calendarType, input.hostCalendarId ?? existing.hostCalendarId, input.canonicalKey ?? existing.canonicalKey ?? existing.remoteId, now, now, existing.id);
|
|
374
498
|
return getCalendarById(existing.id);
|
|
375
499
|
}
|
|
376
500
|
const id = `calendar_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
377
501
|
getDatabase()
|
|
378
502
|
.prepare(`INSERT INTO calendar_calendars (
|
|
379
|
-
id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
503
|
+
id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed,
|
|
504
|
+
source_id, source_title, source_type, calendar_type, host_calendar_id, canonical_key, last_synced_at, created_at, updated_at
|
|
380
505
|
)
|
|
381
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
382
|
-
.run(id, connectionId, input.remoteId, input.title, input.description ?? "", input.color ?? "#7dd3fc", normalizeTimezone(input.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, now, now, now);
|
|
506
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
507
|
+
.run(id, connectionId, input.remoteId, input.title, input.description ?? "", input.color ?? "#7dd3fc", normalizeTimezone(input.timezone), input.isPrimary ? 1 : 0, input.canWrite === false ? 0 : 1, input.selectedForSync === false ? 0 : 1, input.forgeManaged ? 1 : 0, input.sourceId ?? null, input.sourceTitle ?? null, input.sourceType ?? null, input.calendarType ?? null, input.hostCalendarId ?? null, input.canonicalKey ?? input.remoteId, now, now, now);
|
|
383
508
|
return getCalendarById(id);
|
|
384
509
|
}
|
|
385
510
|
export function listCalendarEvents(query) {
|
|
@@ -415,7 +540,12 @@ export function listCalendarEvents(query) {
|
|
|
415
540
|
WHERE ${clauses.join(" AND ")}
|
|
416
541
|
ORDER BY start_at ASC, title ASC`)
|
|
417
542
|
.all(...params);
|
|
418
|
-
|
|
543
|
+
const activeIds = activeConnectionIds();
|
|
544
|
+
return filterOwnedEntities("calendar_event", rows
|
|
545
|
+
.map(mapEvent)
|
|
546
|
+
.filter((event) => event.ownership !== "external" ||
|
|
547
|
+
event.connectionId === null ||
|
|
548
|
+
activeIds.has(event.connectionId)), query.userIds);
|
|
419
549
|
}
|
|
420
550
|
export function getCalendarEventById(eventId) {
|
|
421
551
|
const row = getDatabase()
|
|
@@ -529,10 +659,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
529
659
|
calendarId: calendar.id,
|
|
530
660
|
remoteCalendarId: calendar.remoteId,
|
|
531
661
|
remoteEventId: input.remoteId,
|
|
532
|
-
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
662
|
+
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
663
|
+
? String(input.rawPayload.uid)
|
|
664
|
+
: typeof input.rawPayload?.externalId === "string"
|
|
665
|
+
? String(input.rawPayload.externalId)
|
|
666
|
+
: typeof input.rawPayload?.iCalUID === "string"
|
|
667
|
+
? String(input.rawPayload.iCalUID)
|
|
668
|
+
: typeof input.rawPayload?.iCalUId === "string"
|
|
669
|
+
? String(input.rawPayload.iCalUId)
|
|
670
|
+
: null,
|
|
533
671
|
recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
|
|
534
672
|
? String(input.rawPayload.recurrenceid)
|
|
535
|
-
:
|
|
673
|
+
: typeof input.rawPayload?.occurrenceDate === "string"
|
|
674
|
+
? String(input.rawPayload.occurrenceDate)
|
|
675
|
+
: null,
|
|
536
676
|
isMasterRecurring: Boolean(input.rawPayload?.rrule),
|
|
537
677
|
remoteHref: input.remoteHref ?? null,
|
|
538
678
|
remoteEtag: input.remoteEtag ?? null,
|
|
@@ -558,10 +698,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
|
|
|
558
698
|
calendarId: calendar.id,
|
|
559
699
|
remoteCalendarId: calendar.remoteId,
|
|
560
700
|
remoteEventId: input.remoteId,
|
|
561
|
-
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
701
|
+
remoteUid: typeof input.rawPayload?.uid === "string"
|
|
702
|
+
? String(input.rawPayload.uid)
|
|
703
|
+
: typeof input.rawPayload?.externalId === "string"
|
|
704
|
+
? String(input.rawPayload.externalId)
|
|
705
|
+
: typeof input.rawPayload?.iCalUID === "string"
|
|
706
|
+
? String(input.rawPayload.iCalUID)
|
|
707
|
+
: typeof input.rawPayload?.iCalUId === "string"
|
|
708
|
+
? String(input.rawPayload.iCalUId)
|
|
709
|
+
: null,
|
|
562
710
|
recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
|
|
563
711
|
? String(input.rawPayload.recurrenceid)
|
|
564
|
-
:
|
|
712
|
+
: typeof input.rawPayload?.occurrenceDate === "string"
|
|
713
|
+
? String(input.rawPayload.occurrenceDate)
|
|
714
|
+
: null,
|
|
565
715
|
isMasterRecurring: Boolean(input.rawPayload?.rrule),
|
|
566
716
|
remoteHref: input.remoteHref ?? null,
|
|
567
717
|
remoteEtag: input.remoteEtag ?? null,
|
|
@@ -597,6 +747,20 @@ export function createCalendarEvent(input) {
|
|
|
597
747
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
598
748
|
.run(id, preferredCalendar?.connectionId ?? null, preferredCalendar?.id ?? null, "forge", "native", "confirmed", input.title, input.description, input.location, place.label || input.location, place.address, place.timezone, place.latitude, place.longitude, place.source, place.externalPlaceId, input.startAt, input.endAt, normalizeTimezone(input.timezone), input.isAllDay ? 1 : 0, input.availability, input.eventType, JSON.stringify(input.categories), now, now);
|
|
599
749
|
replaceEventLinks(id, input.links);
|
|
750
|
+
upsertEntityActionProfile({
|
|
751
|
+
entityType: "calendar_event",
|
|
752
|
+
entityId: id,
|
|
753
|
+
profile: buildCalendarEventActionProfile({
|
|
754
|
+
eventId: id,
|
|
755
|
+
title: input.title,
|
|
756
|
+
eventType: input.eventType,
|
|
757
|
+
availability: input.availability,
|
|
758
|
+
startAt: input.startAt,
|
|
759
|
+
endAt: input.endAt,
|
|
760
|
+
activityPresetKey: input.activityPresetKey ?? null,
|
|
761
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour ?? null
|
|
762
|
+
})
|
|
763
|
+
});
|
|
600
764
|
setEntityOwner("calendar_event", id, inferCalendarEventOwnerId(input));
|
|
601
765
|
return getCalendarEventById(id);
|
|
602
766
|
}
|
|
@@ -652,6 +816,34 @@ export function updateCalendarEvent(eventId, patch) {
|
|
|
652
816
|
if (patch.links) {
|
|
653
817
|
replaceEventLinks(eventId, patch.links);
|
|
654
818
|
}
|
|
819
|
+
if (patch.title !== undefined ||
|
|
820
|
+
patch.startAt !== undefined ||
|
|
821
|
+
patch.endAt !== undefined ||
|
|
822
|
+
patch.availability !== undefined ||
|
|
823
|
+
patch.eventType !== undefined ||
|
|
824
|
+
patch.activityPresetKey !== undefined ||
|
|
825
|
+
patch.customSustainRateApPerHour !== undefined) {
|
|
826
|
+
upsertEntityActionProfile({
|
|
827
|
+
entityType: "calendar_event",
|
|
828
|
+
entityId: eventId,
|
|
829
|
+
profile: buildCalendarEventActionProfile({
|
|
830
|
+
eventId,
|
|
831
|
+
title: next.title,
|
|
832
|
+
eventType: next.eventType,
|
|
833
|
+
availability: next.availability,
|
|
834
|
+
startAt: next.startAt,
|
|
835
|
+
endAt: next.endAt,
|
|
836
|
+
activityPresetKey: patch.activityPresetKey === undefined
|
|
837
|
+
? current.actionProfile?.metadata?.activityPresetKey
|
|
838
|
+
: patch.activityPresetKey,
|
|
839
|
+
customSustainRateApPerHour: patch.customSustainRateApPerHour === undefined
|
|
840
|
+
? (typeof current.actionProfile?.metadata?.customSustainRateApPerHour === "number"
|
|
841
|
+
? current.actionProfile.metadata.customSustainRateApPerHour
|
|
842
|
+
: null)
|
|
843
|
+
: patch.customSustainRateApPerHour
|
|
844
|
+
})
|
|
845
|
+
});
|
|
846
|
+
}
|
|
655
847
|
if (patch.userId !== undefined || patch.links !== undefined) {
|
|
656
848
|
setEntityOwner("calendar_event", eventId, patch.userId === undefined
|
|
657
849
|
? inferCalendarEventOwnerId({
|
|
@@ -699,6 +891,19 @@ export function createWorkBlockTemplate(input) {
|
|
|
699
891
|
)
|
|
700
892
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
701
893
|
.run(id, input.title, input.kind, input.color, normalizeTimezone(input.timezone), JSON.stringify(input.weekDays), input.startMinute, input.endMinute, input.startsOn ?? null, input.endsOn ?? null, input.blockingState, now, now);
|
|
894
|
+
upsertEntityActionProfile({
|
|
895
|
+
entityType: "work_block_template",
|
|
896
|
+
entityId: id,
|
|
897
|
+
profile: buildWorkBlockTemplateActionProfile({
|
|
898
|
+
templateId: id,
|
|
899
|
+
title: input.title,
|
|
900
|
+
kind: input.kind,
|
|
901
|
+
startMinute: input.startMinute,
|
|
902
|
+
endMinute: input.endMinute,
|
|
903
|
+
activityPresetKey: input.activityPresetKey ?? null,
|
|
904
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour ?? null
|
|
905
|
+
})
|
|
906
|
+
});
|
|
702
907
|
setEntityOwner("work_block_template", id, input.userId);
|
|
703
908
|
return getWorkBlockTemplateById(id);
|
|
704
909
|
});
|
|
@@ -744,6 +949,32 @@ export function updateWorkBlockTemplate(templateId, patch) {
|
|
|
744
949
|
SET title = ?, kind = ?, color = ?, timezone = ?, weekdays_json = ?, start_minute = ?, end_minute = ?, starts_on = ?, ends_on = ?, blocking_state = ?, updated_at = ?
|
|
745
950
|
WHERE id = ?`)
|
|
746
951
|
.run(next.title, next.kind, next.color, next.timezone, JSON.stringify(next.weekDays), next.startMinute, next.endMinute, next.startsOn, next.endsOn, next.blockingState, next.updatedAt, templateId);
|
|
952
|
+
if (patch.title !== undefined ||
|
|
953
|
+
patch.kind !== undefined ||
|
|
954
|
+
patch.startMinute !== undefined ||
|
|
955
|
+
patch.endMinute !== undefined ||
|
|
956
|
+
patch.activityPresetKey !== undefined ||
|
|
957
|
+
patch.customSustainRateApPerHour !== undefined) {
|
|
958
|
+
upsertEntityActionProfile({
|
|
959
|
+
entityType: "work_block_template",
|
|
960
|
+
entityId: templateId,
|
|
961
|
+
profile: buildWorkBlockTemplateActionProfile({
|
|
962
|
+
templateId,
|
|
963
|
+
title: next.title,
|
|
964
|
+
kind: next.kind,
|
|
965
|
+
startMinute: next.startMinute,
|
|
966
|
+
endMinute: next.endMinute,
|
|
967
|
+
activityPresetKey: patch.activityPresetKey === undefined
|
|
968
|
+
? current.actionProfile?.metadata?.activityPresetKey
|
|
969
|
+
: patch.activityPresetKey,
|
|
970
|
+
customSustainRateApPerHour: patch.customSustainRateApPerHour === undefined
|
|
971
|
+
? (typeof current.actionProfile?.metadata?.customSustainRateApPerHour === "number"
|
|
972
|
+
? current.actionProfile.metadata.customSustainRateApPerHour
|
|
973
|
+
: null)
|
|
974
|
+
: patch.customSustainRateApPerHour
|
|
975
|
+
})
|
|
976
|
+
});
|
|
977
|
+
}
|
|
747
978
|
if (patch.userId !== undefined) {
|
|
748
979
|
setEntityOwner("work_block_template", templateId, patch.userId);
|
|
749
980
|
}
|
|
@@ -790,6 +1021,7 @@ function deriveWorkBlockInstances(template, query) {
|
|
|
790
1021
|
color: template.color,
|
|
791
1022
|
blockingState: template.blockingState,
|
|
792
1023
|
calendarEventId: null,
|
|
1024
|
+
actionProfile: template.actionProfile ?? null,
|
|
793
1025
|
createdAt: template.createdAt,
|
|
794
1026
|
updatedAt: template.updatedAt
|
|
795
1027
|
}));
|
|
@@ -822,7 +1054,10 @@ export function listTaskTimeboxes(query) {
|
|
|
822
1054
|
WHERE ${clauses.join(" AND ")}
|
|
823
1055
|
ORDER BY starts_at ASC`)
|
|
824
1056
|
.all(...params);
|
|
825
|
-
|
|
1057
|
+
const activeIds = activeConnectionIds();
|
|
1058
|
+
return filterOwnedEntities("task_timebox", rows
|
|
1059
|
+
.map(mapTimebox)
|
|
1060
|
+
.filter((timebox) => timebox.connectionId === null || activeIds.has(timebox.connectionId)), query.userIds);
|
|
826
1061
|
}
|
|
827
1062
|
export function getTaskTimeboxById(timeboxId) {
|
|
828
1063
|
const row = getDatabase()
|
|
@@ -836,12 +1071,27 @@ export function getTaskTimeboxById(timeboxId) {
|
|
|
836
1071
|
export function createTaskTimebox(input) {
|
|
837
1072
|
const now = nowIso();
|
|
838
1073
|
const id = `timebox_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
1074
|
+
const task = getTaskById(input.taskId);
|
|
839
1075
|
getDatabase()
|
|
840
1076
|
.prepare(`INSERT INTO task_timeboxes (
|
|
841
1077
|
id, task_id, project_id, connection_id, calendar_id, linked_task_run_id, status, source, title, starts_at, ends_at, override_reason, created_at, updated_at
|
|
842
1078
|
)
|
|
843
1079
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
844
1080
|
.run(id, input.taskId, input.projectId ?? null, input.connectionId ?? null, input.calendarId ?? null, input.linkedTaskRunId ?? null, input.status ?? "planned", input.source ?? "manual", input.title, input.startsAt, input.endsAt, input.overrideReason ?? null, now, now);
|
|
1081
|
+
upsertEntityActionProfile({
|
|
1082
|
+
entityType: "task_timebox",
|
|
1083
|
+
entityId: id,
|
|
1084
|
+
profile: buildTaskTimeboxActionProfile({
|
|
1085
|
+
timeboxId: id,
|
|
1086
|
+
title: input.title,
|
|
1087
|
+
taskId: input.taskId,
|
|
1088
|
+
taskPlannedDurationSeconds: task?.plannedDurationSeconds ?? null,
|
|
1089
|
+
startsAt: input.startsAt,
|
|
1090
|
+
endsAt: input.endsAt,
|
|
1091
|
+
activityPresetKey: input.activityPresetKey ?? null,
|
|
1092
|
+
customSustainRateApPerHour: input.customSustainRateApPerHour ?? null
|
|
1093
|
+
})
|
|
1094
|
+
});
|
|
845
1095
|
setEntityOwner("task_timebox", id, inferTaskTimeboxOwnerId(input));
|
|
846
1096
|
return getTaskTimeboxById(id);
|
|
847
1097
|
}
|
|
@@ -869,6 +1119,33 @@ export function updateTaskTimebox(timeboxId, patch) {
|
|
|
869
1119
|
starts_at = ?, ends_at = ?, override_reason = ?, updated_at = ?
|
|
870
1120
|
WHERE id = ?`)
|
|
871
1121
|
.run(next.connectionId, next.calendarId, next.remoteEventId, next.linkedTaskRunId, next.status, next.source, next.title, next.startsAt, next.endsAt, next.overrideReason, next.updatedAt, timeboxId);
|
|
1122
|
+
if (patch.title !== undefined ||
|
|
1123
|
+
patch.startsAt !== undefined ||
|
|
1124
|
+
patch.endsAt !== undefined ||
|
|
1125
|
+
patch.activityPresetKey !== undefined ||
|
|
1126
|
+
patch.customSustainRateApPerHour !== undefined) {
|
|
1127
|
+
const task = getTaskById(current.taskId);
|
|
1128
|
+
upsertEntityActionProfile({
|
|
1129
|
+
entityType: "task_timebox",
|
|
1130
|
+
entityId: timeboxId,
|
|
1131
|
+
profile: buildTaskTimeboxActionProfile({
|
|
1132
|
+
timeboxId,
|
|
1133
|
+
title: next.title,
|
|
1134
|
+
taskId: current.taskId,
|
|
1135
|
+
taskPlannedDurationSeconds: task?.plannedDurationSeconds ?? null,
|
|
1136
|
+
startsAt: next.startsAt,
|
|
1137
|
+
endsAt: next.endsAt,
|
|
1138
|
+
activityPresetKey: patch.activityPresetKey === undefined
|
|
1139
|
+
? current.actionProfile?.metadata?.activityPresetKey
|
|
1140
|
+
: patch.activityPresetKey,
|
|
1141
|
+
customSustainRateApPerHour: patch.customSustainRateApPerHour === undefined
|
|
1142
|
+
? (typeof current.actionProfile?.metadata?.customSustainRateApPerHour === "number"
|
|
1143
|
+
? current.actionProfile.metadata.customSustainRateApPerHour
|
|
1144
|
+
: null)
|
|
1145
|
+
: patch.customSustainRateApPerHour
|
|
1146
|
+
})
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
872
1149
|
if (patch.userId !== undefined) {
|
|
873
1150
|
setEntityOwner("task_timebox", timeboxId, patch.userId);
|
|
874
1151
|
}
|
|
@@ -1151,6 +1428,12 @@ export function getCalendarOverview(query) {
|
|
|
1151
1428
|
label: "Custom CalDAV",
|
|
1152
1429
|
supportsDedicatedForgeCalendar: true,
|
|
1153
1430
|
connectionHelp: "Use an account-level CalDAV base URL, then let Forge discover the calendars before selecting sync and write targets."
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
provider: "macos_local",
|
|
1434
|
+
label: "Calendars On This Mac",
|
|
1435
|
+
supportsDedicatedForgeCalendar: true,
|
|
1436
|
+
connectionHelp: "Use EventKit to access the calendars already configured in Calendar.app on this Mac. Forge replaces overlapping remote account connections instead of showing duplicate copies."
|
|
1154
1437
|
}
|
|
1155
1438
|
],
|
|
1156
1439
|
connections: listCalendarConnections().map(({ credentialsSecretId: _secret, ...connection }) => connection),
|
|
@@ -70,9 +70,9 @@ function nextSortOrder(status) {
|
|
|
70
70
|
.get(status);
|
|
71
71
|
return row.max_sort + 1;
|
|
72
72
|
}
|
|
73
|
-
function normalizeCompletedAt(status, existingCompletedAt) {
|
|
73
|
+
function normalizeCompletedAt(status, existingCompletedAt, overrideCompletedAt) {
|
|
74
74
|
if (status === "done") {
|
|
75
|
-
return existingCompletedAt ?? new Date().toISOString();
|
|
75
|
+
return overrideCompletedAt ?? existingCompletedAt ?? new Date().toISOString();
|
|
76
76
|
}
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
@@ -203,10 +203,32 @@ function updateTaskRecord(current, input, activity) {
|
|
|
203
203
|
const movedColumns = nextStatus !== current.status;
|
|
204
204
|
const nextSort = input.sortOrder ??
|
|
205
205
|
(movedColumns ? nextSortOrder(nextStatus) : current.sortOrder);
|
|
206
|
+
const completionRequirement = nextStatus === "done"
|
|
207
|
+
? getTaskCompletionRequirement(current, current.userId ?? undefined)
|
|
208
|
+
: null;
|
|
209
|
+
const applyCompletionWorkLogAdjustment = (desiredTodaySeconds, currentTodayCreditedSeconds) => {
|
|
210
|
+
const deltaMinutes = Math.round((desiredTodaySeconds - currentTodayCreditedSeconds) / 60);
|
|
211
|
+
if (deltaMinutes === 0) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const appliedDeltaMinutes = deltaMinutes;
|
|
215
|
+
createWorkAdjustment({
|
|
216
|
+
entityType: "task",
|
|
217
|
+
entityId: current.id,
|
|
218
|
+
deltaMinutes: appliedDeltaMinutes,
|
|
219
|
+
appliedDeltaMinutes,
|
|
220
|
+
note: desiredTodaySeconds <= 0
|
|
221
|
+
? "Completion log cleared for today"
|
|
222
|
+
: "Completion log adjusted for today"
|
|
223
|
+
}, {
|
|
224
|
+
actor: activity?.actor ?? null,
|
|
225
|
+
source: activity?.source ?? "ui"
|
|
226
|
+
});
|
|
227
|
+
};
|
|
206
228
|
if (current.status !== "done" &&
|
|
207
229
|
nextStatus === "done" &&
|
|
208
|
-
input.resolutionKind !== "split"
|
|
209
|
-
|
|
230
|
+
input.resolutionKind !== "split" &&
|
|
231
|
+
completionRequirement) {
|
|
210
232
|
if (input.enforceTodayWorkLog === true &&
|
|
211
233
|
completionRequirement.requiresWorkLog &&
|
|
212
234
|
input.completedTodayWorkSeconds === undefined) {
|
|
@@ -215,21 +237,18 @@ function updateTaskRecord(current, input, activity) {
|
|
|
215
237
|
todayCreditedSeconds: completionRequirement.todayCreditedSeconds
|
|
216
238
|
});
|
|
217
239
|
}
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
entityType: "task",
|
|
222
|
-
entityId: current.id,
|
|
223
|
-
deltaMinutes: appliedDeltaMinutes,
|
|
224
|
-
appliedDeltaMinutes,
|
|
225
|
-
note: "Completion log for today"
|
|
226
|
-
}, {
|
|
227
|
-
actor: activity?.actor ?? null,
|
|
228
|
-
source: activity?.source ?? "ui"
|
|
229
|
-
});
|
|
240
|
+
if (input.completedTodayWorkSeconds !== undefined) {
|
|
241
|
+
const desiredTodaySeconds = Math.max(0, input.completedTodayWorkSeconds);
|
|
242
|
+
applyCompletionWorkLogAdjustment(desiredTodaySeconds, completionRequirement.todayCreditedSeconds);
|
|
230
243
|
}
|
|
231
244
|
}
|
|
232
|
-
|
|
245
|
+
else if (nextStatus === "done" &&
|
|
246
|
+
input.completedTodayWorkSeconds !== undefined &&
|
|
247
|
+
completionRequirement) {
|
|
248
|
+
const desiredTodaySeconds = Math.max(0, input.completedTodayWorkSeconds);
|
|
249
|
+
applyCompletionWorkLogAdjustment(desiredTodaySeconds, completionRequirement.todayCreditedSeconds);
|
|
250
|
+
}
|
|
251
|
+
const completedAt = normalizeCompletedAt(nextStatus, current.completedAt, input.completedAt);
|
|
233
252
|
const updatedAt = new Date().toISOString();
|
|
234
253
|
getDatabase()
|
|
235
254
|
.prepare(`UPDATE tasks
|