forge-openclaw-plugin 0.2.28 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{board-DPFvZf-D.js → board-q8cfwaAW.js} +2 -2
  3. package/dist/assets/{board-DPFvZf-D.js.map → board-q8cfwaAW.js.map} +1 -1
  4. package/dist/assets/index-CPC6E84V.js +85 -0
  5. package/dist/assets/index-CPC6E84V.js.map +1 -0
  6. package/dist/assets/index-DiyKCDxL.css +1 -0
  7. package/dist/assets/{motion-Bvwc85ch.js → motion-DHfqFntt.js} +2 -2
  8. package/dist/assets/{motion-Bvwc85ch.js.map → motion-DHfqFntt.js.map} +1 -1
  9. package/dist/assets/{table-FJQTJvUR.js → table-DLweENXt.js} +2 -2
  10. package/dist/assets/{table-FJQTJvUR.js.map → table-DLweENXt.js.map} +1 -1
  11. package/dist/assets/{ui-GXFcgvSw.js → ui-BV0OYxkH.js} +2 -2
  12. package/dist/assets/{ui-GXFcgvSw.js.map → ui-BV0OYxkH.js.map} +1 -1
  13. package/dist/assets/{vendor-Cwf49UMz.js → vendor-OwcH20PM.js} +2 -2
  14. package/dist/assets/{vendor-Cwf49UMz.js.map → vendor-OwcH20PM.js.map} +1 -1
  15. package/dist/index.html +7 -7
  16. package/dist/server/server/migrations/044_macos_local_calendar_provider.sql +21 -0
  17. package/dist/server/server/src/app.js +113 -17
  18. package/dist/server/server/src/movement.js +151 -0
  19. package/dist/server/server/src/openapi.js +29 -1
  20. package/dist/server/server/src/repositories/calendar.js +144 -12
  21. package/dist/server/server/src/repositories/tasks.js +36 -17
  22. package/dist/server/server/src/services/calendar-runtime.js +613 -32
  23. package/dist/server/server/src/services/life-force.js +84 -52
  24. package/dist/server/server/src/services/macos-calendar-helper.js +748 -0
  25. package/dist/server/server/src/types.js +46 -2
  26. package/dist/server/src/lib/api-error.js +2 -0
  27. package/dist/server/src/lib/api.js +51 -2
  28. package/dist/server/src/lib/calendar-name-deduper.js +2 -0
  29. package/openclaw.plugin.json +1 -1
  30. package/package.json +1 -1
  31. package/server/migrations/044_macos_local_calendar_provider.sql +21 -0
  32. package/skills/forge-openclaw/SKILL.md +40 -7
  33. package/skills/forge-openclaw/entity_conversation_playbooks.md +88 -5
  34. package/dist/assets/index-Auw3JrdE.css +0 -1
  35. package/dist/assets/index-D1H7myQH.js +0 -85
  36. package/dist/assets/index-D1H7myQH.js.map +0 -1
@@ -64,6 +64,12 @@ function mapCalendar(row) {
64
64
  canWrite: Boolean(row.can_write),
65
65
  selectedForSync: Boolean(row.selected_for_sync),
66
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,
67
73
  lastSyncedAt: row.last_synced_at,
68
74
  createdAt: row.created_at,
69
75
  updatedAt: row.updated_at
@@ -255,6 +261,13 @@ export function readEncryptedSecret(secretId) {
255
261
  export function deleteEncryptedSecret(secretId) {
256
262
  getDatabase().prepare(`DELETE FROM stored_secrets WHERE id = ?`).run(secretId);
257
263
  }
264
+ export function isSupersededCalendarConnection(connectionId) {
265
+ const connection = getCalendarConnectionById(connectionId);
266
+ if (!connection) {
267
+ return false;
268
+ }
269
+ return isSupersededConnection(connection);
270
+ }
258
271
  export function listCalendarConnections() {
259
272
  const rows = getDatabase()
260
273
  .prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
@@ -264,6 +277,15 @@ export function listCalendarConnections() {
264
277
  .all();
265
278
  return rows.map(mapConnection);
266
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
+ }
267
289
  export function getCalendarConnectionById(connectionId) {
268
290
  const row = getDatabase()
269
291
  .prepare(`SELECT id, provider, label, account_label, status, config_json, credentials_secret_id, forge_calendar_id,
@@ -330,6 +352,70 @@ export function deleteExternalEventsForConnection(connectionId) {
330
352
  }
331
353
  return rows.map((row) => row.id);
332
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
+ }
333
419
  export function detachConnectionFromForgeEvents(connectionId) {
334
420
  const now = nowIso();
335
421
  getDatabase()
@@ -352,16 +438,23 @@ export function listCalendars(connectionId, options = {}) {
352
438
  : "WHERE (selected_for_sync = 1 OR forge_managed = 1)";
353
439
  const rows = getDatabase()
354
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,
355
442
  last_synced_at, created_at, updated_at
356
443
  FROM calendar_calendars
357
444
  ${connectionId ? `WHERE connection_id = ? ${visibilityClause}` : visibilityClause}
358
445
  ORDER BY forge_managed DESC, title ASC`)
359
446
  .all(...(connectionId ? [connectionId] : []));
360
- return rows.map(mapCalendar);
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));
361
453
  }
362
454
  export function getCalendarById(calendarId) {
363
455
  const row = getDatabase()
364
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,
365
458
  last_synced_at, created_at, updated_at
366
459
  FROM calendar_calendars
367
460
  WHERE id = ?`)
@@ -371,6 +464,7 @@ export function getCalendarById(calendarId) {
371
464
  function getDefaultWritableCalendar() {
372
465
  const row = getDatabase()
373
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,
374
468
  last_synced_at, created_at, updated_at
375
469
  FROM calendar_calendars
376
470
  WHERE can_write = 1
@@ -383,6 +477,7 @@ function getDefaultWritableCalendar() {
383
477
  export function getCalendarByRemoteId(connectionId, remoteId) {
384
478
  const row = getDatabase()
385
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,
386
481
  last_synced_at, created_at, updated_at
387
482
  FROM calendar_calendars
388
483
  WHERE connection_id = ? AND remote_id = ?`)
@@ -395,18 +490,21 @@ export function upsertCalendarRecord(connectionId, input) {
395
490
  if (existing) {
396
491
  getDatabase()
397
492
  .prepare(`UPDATE calendar_calendars
398
- SET title = ?, description = ?, color = ?, timezone = ?, is_primary = ?, can_write = ?, selected_for_sync = ?, forge_managed = ?, last_synced_at = ?, updated_at = ?
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 = ?
399
496
  WHERE id = ?`)
400
- .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);
401
498
  return getCalendarById(existing.id);
402
499
  }
403
500
  const id = `calendar_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
404
501
  getDatabase()
405
502
  .prepare(`INSERT INTO calendar_calendars (
406
- id, connection_id, remote_id, title, description, color, timezone, is_primary, can_write, selected_for_sync, forge_managed, last_synced_at, created_at, updated_at
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
407
505
  )
408
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
409
- .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);
410
508
  return getCalendarById(id);
411
509
  }
412
510
  export function listCalendarEvents(query) {
@@ -442,7 +540,12 @@ export function listCalendarEvents(query) {
442
540
  WHERE ${clauses.join(" AND ")}
443
541
  ORDER BY start_at ASC, title ASC`)
444
542
  .all(...params);
445
- return filterOwnedEntities("calendar_event", rows.map(mapEvent), query.userIds);
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);
446
549
  }
447
550
  export function getCalendarEventById(eventId) {
448
551
  const row = getDatabase()
@@ -556,10 +659,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
556
659
  calendarId: calendar.id,
557
660
  remoteCalendarId: calendar.remoteId,
558
661
  remoteEventId: input.remoteId,
559
- remoteUid: typeof input.rawPayload?.uid === "string" ? String(input.rawPayload.uid) : null,
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,
560
671
  recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
561
672
  ? String(input.rawPayload.recurrenceid)
562
- : null,
673
+ : typeof input.rawPayload?.occurrenceDate === "string"
674
+ ? String(input.rawPayload.occurrenceDate)
675
+ : null,
563
676
  isMasterRecurring: Boolean(input.rawPayload?.rrule),
564
677
  remoteHref: input.remoteHref ?? null,
565
678
  remoteEtag: input.remoteEtag ?? null,
@@ -585,10 +698,20 @@ export function upsertCalendarEventRecord(connectionId, input) {
585
698
  calendarId: calendar.id,
586
699
  remoteCalendarId: calendar.remoteId,
587
700
  remoteEventId: input.remoteId,
588
- remoteUid: typeof input.rawPayload?.uid === "string" ? String(input.rawPayload.uid) : null,
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,
589
710
  recurrenceInstanceId: typeof input.rawPayload?.recurrenceid === "string"
590
711
  ? String(input.rawPayload.recurrenceid)
591
- : null,
712
+ : typeof input.rawPayload?.occurrenceDate === "string"
713
+ ? String(input.rawPayload.occurrenceDate)
714
+ : null,
592
715
  isMasterRecurring: Boolean(input.rawPayload?.rrule),
593
716
  remoteHref: input.remoteHref ?? null,
594
717
  remoteEtag: input.remoteEtag ?? null,
@@ -931,7 +1054,10 @@ export function listTaskTimeboxes(query) {
931
1054
  WHERE ${clauses.join(" AND ")}
932
1055
  ORDER BY starts_at ASC`)
933
1056
  .all(...params);
934
- return filterOwnedEntities("task_timebox", rows.map(mapTimebox), query.userIds);
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);
935
1061
  }
936
1062
  export function getTaskTimeboxById(timeboxId) {
937
1063
  const row = getDatabase()
@@ -1302,6 +1428,12 @@ export function getCalendarOverview(query) {
1302
1428
  label: "Custom CalDAV",
1303
1429
  supportsDedicatedForgeCalendar: true,
1304
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."
1305
1437
  }
1306
1438
  ],
1307
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
- const completionRequirement = getTaskCompletionRequirement(current, current.userId ?? undefined);
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 ((input.completedTodayWorkSeconds ?? 0) > 0) {
219
- const appliedDeltaMinutes = Math.max(1, Math.round((input.completedTodayWorkSeconds ?? 0) / 60));
220
- createWorkAdjustment({
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
- const completedAt = normalizeCompletedAt(nextStatus, current.completedAt);
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