forge-openclaw-plugin 0.2.98 → 0.2.100

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 (149) hide show
  1. package/dist/assets/activity-copy-Bj4h9OcF.js +1 -0
  2. package/dist/assets/activity-page-5oyCFOns.js +1 -0
  3. package/dist/assets/ai-surface-workspace-qgk_B57-.js +1 -0
  4. package/dist/assets/atlas-panel-rfH2qOez.js +1 -0
  5. package/dist/assets/{board-Ju0h0SeG.js → board-BkDRaMp6.js} +1 -1
  6. package/dist/assets/calendar-display-preferences-Cid-2RnL.js +1 -0
  7. package/dist/assets/calendar-page-Bo2iua-a.js +1 -0
  8. package/dist/assets/calendar-rules-DA1g3QUk.js +1 -0
  9. package/dist/assets/calendar-ui-Cy1XRwzV.js +1 -0
  10. package/dist/assets/calendar-week-toolbar-DU1Q4RYj.js +1 -0
  11. package/dist/assets/charts-P7EVhIog.js +36 -0
  12. package/dist/assets/companion-sync-lab-page-CosNknOK.js +1 -0
  13. package/dist/assets/daily-metrics-dashboard-LjuGAB3f.js +1 -0
  14. package/dist/assets/date-keys-Cj1G3TOn.js +1 -0
  15. package/dist/assets/entity-links-DwpxhW2H.js +1 -0
  16. package/dist/assets/entity-note-count-link-BmGDB572.js +1 -0
  17. package/dist/assets/entity-notes-surface-DgEgicaE.js +1 -0
  18. package/dist/assets/execution-board-CDRXQB85.js +1 -0
  19. package/dist/assets/faceted-token-search-CE1YauRd.js +1 -0
  20. package/dist/assets/flagship-signal-deck-DDds90Gl.js +1 -0
  21. package/dist/assets/floating-action-menu-CJkI2iFy.js +1 -0
  22. package/dist/assets/forms-BFlTgZ3W.js +1 -0
  23. package/dist/assets/goal-detail-page-cJvHaLMQ.js +1 -0
  24. package/dist/assets/goals-page-f_39hvUV.js +1 -0
  25. package/dist/assets/graph-BZV40eAE.css +1 -0
  26. package/dist/assets/graph-D6JLqDbD.js +318 -0
  27. package/dist/assets/habits-page-DKb96_mj.js +1 -0
  28. package/dist/assets/health-link-options-Cpx8w7uM.js +1 -0
  29. package/dist/assets/index-BHTUu_4M.js +19 -0
  30. package/dist/assets/index-CZbuZQjw.css +1 -0
  31. package/dist/assets/insight-flow-dialog-pzAzyayN.js +1 -0
  32. package/dist/assets/insights-page-Dc9oFltJ.js +8 -0
  33. package/dist/assets/kanban-page-JAxerYh6.js +1 -0
  34. package/dist/assets/knowledge-graph-page-UQ3skqEi.js +1 -0
  35. package/dist/assets/life-force-page-BGDbQuVh.js +1 -0
  36. package/dist/assets/life-force-workspace-B1fYSXRC.js +1 -0
  37. package/dist/assets/maps-B-YMMjus.css +1 -0
  38. package/dist/assets/maps-ClgJoCjz.js +803 -0
  39. package/dist/assets/metric-tile-DX6TclqM.js +1 -0
  40. package/dist/assets/{motion-DRPJkN3a.js → motion-BeD44FeG.js} +1 -1
  41. package/dist/assets/movement-page-6HP6nGJx.js +1 -0
  42. package/dist/assets/note-markdown-DiW2-5d3.js +3 -0
  43. package/dist/assets/note-tags-input-DDLXf54U.js +1 -0
  44. package/dist/assets/notes-page-BuguDjhz.js +1 -0
  45. package/dist/assets/open-in-graph-button-Cg5VrKsC.js +1 -0
  46. package/dist/assets/orbit-map-GD05-0oS.js +1 -0
  47. package/dist/assets/overview-page-DuOs2OCB.js +1 -0
  48. package/dist/assets/page-hero-CQWo1Mm_.js +1 -0
  49. package/dist/assets/pill-cluster-BJogDRDJ.js +1 -0
  50. package/dist/assets/preference-entity-handoff-button-D4WAs9pC.js +1 -0
  51. package/dist/assets/preferences-page-BaJTMU1I.js +1 -0
  52. package/dist/assets/project-collections-DvaX20q_.js +1 -0
  53. package/dist/assets/project-detail-page-drPIFZGb.js +1 -0
  54. package/dist/assets/project-management-hierarchy-page-BUbRXvny.js +1 -0
  55. package/dist/assets/project-management-section-nav-C2Ud8Zdd.js +1 -0
  56. package/dist/assets/projects-page-BGzEZUtg.js +1 -0
  57. package/dist/assets/psyche-behaviors-page-Dmm_Io9D.js +5 -0
  58. package/dist/assets/psyche-flashcards-page-BgNKJ6QJ.js +1 -0
  59. package/dist/assets/psyche-goal-map-page-DXJs98Vr.js +1 -0
  60. package/dist/assets/psyche-graph-CFgs_Bqc.js +1 -0
  61. package/dist/assets/psyche-metrics-page-zYTJDbyZ.js +1 -0
  62. package/dist/assets/psyche-mode-guide-page-XPgRfCOf.js +1 -0
  63. package/dist/assets/psyche-modes-page-B-GA8oRF.js +1 -0
  64. package/dist/assets/psyche-page--r6a3e1t.js +1 -0
  65. package/dist/assets/psyche-patterns-page-BM5-3bMm.js +5 -0
  66. package/dist/assets/psyche-questionnaire-builder-page-CJshQ-mg.js +1 -0
  67. package/dist/assets/psyche-questionnaire-detail-page-USmR5G5A.js +1 -0
  68. package/dist/assets/psyche-questionnaire-run-detail-page-D7iBCmTi.js +1 -0
  69. package/dist/assets/psyche-questionnaire-run-page-Cpil-kDh.js +1 -0
  70. package/dist/assets/psyche-questionnaires-page-C-_y3VwS.js +1 -0
  71. package/dist/assets/psyche-report-detail-page--dkSPRaj.js +3 -0
  72. package/dist/assets/psyche-reports-page-CUaOXmIN.js +1 -0
  73. package/dist/assets/psyche-schemas-HFmg37Wj.js +1 -0
  74. package/dist/assets/psyche-schemas-beliefs-page-BX6xaap3.js +9 -0
  75. package/dist/assets/psyche-screen-time-page-CAAI4mD7.js +1 -0
  76. package/dist/assets/psyche-self-observation-page-BZ6FLuwa.js +1 -0
  77. package/dist/assets/psyche-values-page-yEV6MGt8.js +5 -0
  78. package/dist/assets/query-cache-IQ8W-LNC.js +1 -0
  79. package/dist/assets/report-chain-fields-fZ8Xd4H6.js +1 -0
  80. package/dist/assets/rewards-page-C2HQjIAf.js +1 -0
  81. package/dist/assets/scheduling-rules-editor-BHOpHOrV.js +1 -0
  82. package/dist/assets/schema-badge-DyKbxb51.js +1 -0
  83. package/dist/assets/schema-visuals-D6nxjbYC.js +1 -0
  84. package/dist/assets/select-menu-BX-pZNqL.js +1 -0
  85. package/dist/assets/settings-agents-page-VuYXTiyc.js +6 -0
  86. package/dist/assets/settings-bin-page-BNzvYaOk.js +1 -0
  87. package/dist/assets/settings-calendar-page-CjSFB53S.js +5 -0
  88. package/dist/assets/settings-data-page-CGSlryuI.js +1 -0
  89. package/dist/assets/settings-logs-page-BTK5fine.js +1 -0
  90. package/dist/assets/settings-mobile-page-CRaObOGo.js +1 -0
  91. package/dist/assets/settings-models-page-DFshpYF8.js +1 -0
  92. package/dist/assets/settings-page-BP81Mb5R.js +1 -0
  93. package/dist/assets/settings-rewards-page-CDJ1PH2G.js +1 -0
  94. package/dist/assets/settings-section-nav-CCFm27r2.js +1 -0
  95. package/dist/assets/settings-users-page-TdUocFPa.js +1 -0
  96. package/dist/assets/settings-wiki-page-B2zX0QQG.js +1 -0
  97. package/dist/assets/sleep-page-cI1GMVzk.js +1 -0
  98. package/dist/assets/sports-page-06LTqp0V.js +1 -0
  99. package/dist/assets/state-B-4sS1xO.js +1 -0
  100. package/dist/assets/strategies-page-DXP9Kx8s.js +1 -0
  101. package/dist/assets/strategy-detail-page-D6mx_Mik.js +1 -0
  102. package/dist/assets/strategy-dialog-BvzomTaF.js +1 -0
  103. package/dist/assets/{table-DewbFlTh.js → table-WfAPUppN.js} +1 -1
  104. package/dist/assets/task-detail-page-BIWIggdp.js +1 -0
  105. package/dist/assets/timebox-planning-dialog-CaCnoslG.js +1 -0
  106. package/dist/assets/today-page-DO2mRPT2.js +1 -0
  107. package/dist/assets/training-load-page-CyZ0mlEr.js +1 -0
  108. package/dist/assets/{ui-C2IvSrAz.js → ui-C13Nbgas.js} +4 -4
  109. package/dist/assets/use-psyche-focus-target-C1C_XjYG.js +1 -0
  110. package/dist/assets/vendor-CRS-psbw.css +1 -0
  111. package/dist/assets/vendor-DHkYh85p.js +1052 -0
  112. package/dist/assets/vitals-page-BQvEjTc6.js +1 -0
  113. package/dist/assets/weekly-review-page-Tp6Q9CRj.js +1 -0
  114. package/dist/assets/weight-loss-page-BBzlhLVV.js +1 -0
  115. package/dist/assets/wiki-article-markdown-DQYohmW2.js +4 -0
  116. package/dist/assets/wiki-editor-page-Dem_3eZv.js +26 -0
  117. package/dist/assets/wiki-ingest-history-page-BxoOcCoJ.js +1 -0
  118. package/dist/assets/wiki-ingest-modal-DhguKk3J.js +1 -0
  119. package/dist/assets/wiki-page-BLRxVXkl.js +1 -0
  120. package/dist/assets/workbench-flow-page-DqMkCCTy.js +5 -0
  121. package/dist/assets/workbench-page-BWd02wPw.js +1 -0
  122. package/dist/assets/workout-detail-page-BD8u7GyL.js +2 -0
  123. package/dist/index.html +148 -9
  124. package/dist/openclaw/tools.js +340 -0
  125. package/dist/server/server/migrations/065_weight_loss_nutrition_insights.sql +236 -0
  126. package/dist/server/server/migrations/066_watch_action_receipts.sql +20 -0
  127. package/dist/server/server/src/app.js +266 -13
  128. package/dist/server/server/src/health-weight-loss.js +1378 -0
  129. package/dist/server/server/src/health.js +188 -35
  130. package/dist/server/server/src/openapi.js +449 -0
  131. package/dist/server/server/src/services/context.js +6 -7
  132. package/dist/server/server/src/services/doctor.js +39 -4
  133. package/dist/server/server/src/services/gamification.js +146 -34
  134. package/dist/server/server/src/watch-mobile.js +564 -4
  135. package/dist/server/server/src/web.js +18 -5
  136. package/dist/server/src/components/ui/info-tooltip.js +48 -3
  137. package/dist/server/src/lib/api.js +131 -0
  138. package/dist/server/src/lib/weight-loss-types.js +1 -0
  139. package/openclaw.plugin.json +14 -1
  140. package/package.json +1 -1
  141. package/server/migrations/065_weight_loss_nutrition_insights.sql +236 -0
  142. package/server/migrations/066_watch_action_receipts.sql +20 -0
  143. package/skills/forge-openclaw/SKILL.md +26 -5
  144. package/skills/forge-openclaw/entity_conversation_playbooks.md +134 -5
  145. package/skills/forge-openclaw/psyche_entity_playbooks.md +45 -0
  146. package/dist/assets/index-Cn5Wpwau.css +0 -1
  147. package/dist/assets/index-CwvGs8n4.js +0 -91
  148. package/dist/assets/vendor-B-Lq_OG3.css +0 -1
  149. package/dist/assets/vendor-DL2K5ayT.js +0 -2186
@@ -1,10 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { z } from "zod";
2
+ import { z, ZodError } from "zod";
3
3
  import { getDatabase, runInTransaction } from "./db.js";
4
4
  import { HttpError } from "./errors.js";
5
5
  import { updateWorkoutMetadata } from "./health.js";
6
6
  import { canonicalizeMovementCategoryTags, listMovementPlaces, normalizeMovementCategoryTag, updateMovementPlace } from "./movement.js";
7
- import { listHabits } from "./repositories/habits.js";
7
+ import { createHabitCheckIn, listHabits } from "./repositories/habits.js";
8
+ import { listGoals } from "./repositories/goals.js";
9
+ import { listProjectSummaries } from "./services/projects.js";
10
+ import { listTasks, updateTask } from "./repositories/tasks.js";
11
+ import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
8
12
  import { formatLocalDateKey } from "../../src/lib/date-keys.js";
9
13
  const watchCapability = "watch-ready";
10
14
  const watchHistoryStateSchema = z.enum(["aligned", "unaligned", "unknown"]);
@@ -29,6 +33,16 @@ const watchCaptureEventTypeSchema = z.enum([
29
33
  "dictated_note",
30
34
  "retrospective_label"
31
35
  ]);
36
+ const watchCommandKindSchema = z.enum([
37
+ "habit_check_in",
38
+ "capture_event",
39
+ "task_run_start",
40
+ "task_run_heartbeat",
41
+ "task_run_focus",
42
+ "task_run_complete",
43
+ "task_run_release",
44
+ "task_status_update"
45
+ ]);
32
46
  const watchDeviceSchema = z.object({
33
47
  name: z.string().trim().default("Apple Watch"),
34
48
  platform: z.string().trim().default("watchos"),
@@ -72,6 +86,20 @@ export const mobileWatchCaptureBatchSchema = z.object({
72
86
  device: watchDeviceSchema.default({}),
73
87
  events: z.array(watchCaptureEventSchema).max(100).default([])
74
88
  });
89
+ export const mobileWatchCommandBatchSchema = z.object({
90
+ sessionId: z.string().trim().min(1),
91
+ pairingToken: z.string().trim().min(1),
92
+ device: watchDeviceSchema.default({}),
93
+ commands: z
94
+ .array(z.object({
95
+ id: z.string().trim().min(1),
96
+ kind: watchCommandKindSchema,
97
+ createdAt: z.string().datetime(),
98
+ payload: z.record(z.string(), z.unknown()).default({})
99
+ }))
100
+ .max(100)
101
+ .default([])
102
+ });
75
103
  function safeJsonParse(raw, fallback) {
76
104
  if (!raw || raw.trim().length === 0) {
77
105
  return fallback;
@@ -86,9 +114,21 @@ function safeJsonParse(raw, fallback) {
86
114
  function nowIso() {
87
115
  return new Date().toISOString();
88
116
  }
117
+ function watchActorLabel(pairing) {
118
+ const settings = getDatabase()
119
+ .prepare(`SELECT operator_name FROM app_settings WHERE id = 1`)
120
+ .get();
121
+ const operatorName = settings?.operator_name?.trim();
122
+ return operatorName || pairing.user_id || "Albert";
123
+ }
89
124
  function formatDateKey(date) {
90
125
  return formatLocalDateKey(date);
91
126
  }
127
+ function userScopeFilter(pairing) {
128
+ return pairing.user_id === "user_operator"
129
+ ? {}
130
+ : { userIds: [pairing.user_id] };
131
+ }
92
132
  function parseDateKey(dateKey) {
93
133
  const [year, month, day] = dateKey.split("-").map(Number);
94
134
  return new Date(year, month - 1, day);
@@ -477,6 +517,238 @@ export function assertWatchReady(pairing) {
477
517
  throw new HttpError(403, "watch_pairing_not_enabled", "This companion pairing is not allowed to serve watch data.");
478
518
  }
479
519
  }
520
+ function compactTask(task) {
521
+ return {
522
+ id: String(task.id ?? ""),
523
+ title: String(task.title ?? "Untitled work"),
524
+ status: String(task.status ?? "backlog"),
525
+ level: String(task.level ?? "task"),
526
+ priority: String(task.priority ?? "medium"),
527
+ dueDate: typeof task.dueDate === "string" ? task.dueDate : null,
528
+ projectId: typeof task.projectId === "string" ? task.projectId : null,
529
+ goalId: typeof task.goalId === "string" ? task.goalId : null,
530
+ parentWorkItemId: typeof task.parentWorkItemId === "string" ? task.parentWorkItemId : null,
531
+ points: typeof task.points === "number" ? task.points : 0,
532
+ effort: String(task.effort ?? ""),
533
+ energy: String(task.energy ?? ""),
534
+ updatedAt: String(task.updatedAt ?? "")
535
+ };
536
+ }
537
+ function compactTaskRun(run) {
538
+ return {
539
+ id: String(run.id ?? ""),
540
+ taskId: String(run.taskId ?? ""),
541
+ taskTitle: String(run.taskTitle ?? "Active work"),
542
+ actor: String(run.actor ?? ""),
543
+ status: String(run.status ?? ""),
544
+ isCurrent: Boolean(run.isCurrent),
545
+ timerMode: String(run.timerMode ?? "unlimited"),
546
+ plannedDurationSeconds: typeof run.plannedDurationSeconds === "number"
547
+ ? run.plannedDurationSeconds
548
+ : null,
549
+ creditedSeconds: typeof run.creditedSeconds === "number" ? run.creditedSeconds : 0,
550
+ claimedAt: String(run.claimedAt ?? ""),
551
+ heartbeatAt: String(run.heartbeatAt ?? ""),
552
+ leaseExpiresAt: String(run.leaseExpiresAt ?? "")
553
+ };
554
+ }
555
+ function buildWorkSnapshot(pairing) {
556
+ const scope = userScopeFilter(pairing);
557
+ const allTasks = listTasks({ ...scope, limit: 100 }).map(compactTask);
558
+ const activeRuns = listTaskRuns({
559
+ ...scope,
560
+ active: true,
561
+ limit: 12
562
+ }).map(compactTaskRun);
563
+ const visibleTasks = allTasks.filter((task) => task.status !== "done");
564
+ const statuses = ["focus", "in_progress", "blocked", "backlog", "done"];
565
+ const lanes = statuses.map((status) => {
566
+ const laneTasks = allTasks
567
+ .filter((task) => task.status === status)
568
+ .slice(0, status === "done" ? 5 : 12);
569
+ return {
570
+ id: status,
571
+ title: status === "in_progress"
572
+ ? "In progress"
573
+ : status.charAt(0).toUpperCase() + status.slice(1),
574
+ count: allTasks.filter((task) => task.status === status).length,
575
+ tasks: laneTasks
576
+ };
577
+ });
578
+ return {
579
+ actor: watchActorLabel(pairing),
580
+ activeRuns,
581
+ currentRun: activeRuns.find((run) => run.isCurrent) ?? activeRuns[0] ?? null,
582
+ nextTask: visibleTasks.find((task) => task.status === "focus") ??
583
+ visibleTasks.find((task) => task.status === "in_progress") ??
584
+ visibleTasks[0] ??
585
+ null,
586
+ lanes,
587
+ visibleCount: visibleTasks.length,
588
+ doneCount: allTasks.filter((task) => task.status === "done").length
589
+ };
590
+ }
591
+ function buildDirectionSnapshot(pairing) {
592
+ const scope = userScopeFilter(pairing);
593
+ const goals = listGoals()
594
+ .filter((goal) => pairing.user_id === "user_operator" ||
595
+ goal.userId === pairing.user_id)
596
+ .filter((goal) => goal.status === "active")
597
+ .slice(0, 8)
598
+ .map((goal) => ({
599
+ id: goal.id,
600
+ title: goal.title,
601
+ horizon: goal.horizon,
602
+ status: goal.status,
603
+ targetPoints: goal.targetPoints
604
+ }));
605
+ const projects = listProjectSummaries(scope)
606
+ .filter((project) => project.status === "active")
607
+ .slice(0, 8)
608
+ .map((project) => ({
609
+ id: project.id,
610
+ title: project.title,
611
+ status: project.status,
612
+ workflowStatus: project.workflowStatus,
613
+ goalId: project.goalId,
614
+ goalTitle: project.goalTitle,
615
+ activeRunCount: project.time.activeRunCount,
616
+ openTaskCount: project.activeTaskCount
617
+ }));
618
+ return { goals, projects };
619
+ }
620
+ function buildTodaySnapshot(pairing) {
621
+ const todayKey = formatLocalDateKey();
622
+ const tasks = listTasks({ ...userScopeFilter(pairing), limit: 100 }).map(compactTask);
623
+ const dueToday = tasks
624
+ .filter((task) => task.status !== "done" && task.dueDate === todayKey)
625
+ .slice(0, 8);
626
+ return {
627
+ dateKey: todayKey,
628
+ dueTasks: dueToday,
629
+ dueCount: dueToday.length,
630
+ recentDone: tasks.filter((task) => task.status === "done").slice(0, 5)
631
+ };
632
+ }
633
+ function buildHealthSnapshot(userId) {
634
+ const workout = getDatabase()
635
+ .prepare(`SELECT id, workout_type, started_at, ended_at, duration_seconds,
636
+ average_heart_rate, max_heart_rate, derived_json
637
+ FROM health_workout_sessions
638
+ WHERE user_id = ?
639
+ ORDER BY started_at DESC
640
+ LIMIT 1`)
641
+ .get(userId);
642
+ const latestVitals = getDatabase()
643
+ .prepare(`SELECT date_key, metrics_json
644
+ FROM health_daily_summaries
645
+ WHERE user_id = ?
646
+ AND summary_type = 'vitals'
647
+ ORDER BY date_key DESC
648
+ LIMIT 1`)
649
+ .get(userId);
650
+ const latestVitalMetrics = safeJsonParse(latestVitals?.metrics_json, {});
651
+ const workoutDerived = safeJsonParse(workout?.derived_json, {});
652
+ const trainingLoad = typeof workoutDerived.trainingLoad === "number"
653
+ ? workoutDerived.trainingLoad
654
+ : typeof workoutDerived.trimp === "number"
655
+ ? workoutDerived.trimp
656
+ : null;
657
+ const heartRateSampleCount = typeof workoutDerived.heartRateSampleCount === "number"
658
+ ? workoutDerived.heartRateSampleCount
659
+ : typeof workoutDerived.hrSampleCount === "number"
660
+ ? workoutDerived.hrSampleCount
661
+ : 0;
662
+ return {
663
+ lastWorkout: workout
664
+ ? {
665
+ id: workout.id,
666
+ workoutType: workout.workout_type,
667
+ startedAt: workout.started_at,
668
+ endedAt: workout.ended_at,
669
+ durationSeconds: workout.duration_seconds ?? 0,
670
+ averageHeartRate: workout.average_heart_rate,
671
+ maxHeartRate: workout.max_heart_rate,
672
+ trainingLoad,
673
+ heartRateSampleCount
674
+ }
675
+ : null,
676
+ latestVitals: latestVitals
677
+ ? {
678
+ dayKey: latestVitals.date_key,
679
+ metricCount: Object.keys(latestVitalMetrics).length
680
+ }
681
+ : null
682
+ };
683
+ }
684
+ function buildMovementSnapshot(userId) {
685
+ const latestStay = getDatabase()
686
+ .prepare(`SELECT id, label, started_at, ended_at
687
+ FROM movement_stays
688
+ WHERE user_id = ?
689
+ ORDER BY started_at DESC
690
+ LIMIT 1`)
691
+ .get(userId);
692
+ const latestTrip = getDatabase()
693
+ .prepare(`SELECT id, label, started_at, ended_at
694
+ FROM movement_trips
695
+ WHERE user_id = ?
696
+ ORDER BY started_at DESC
697
+ LIMIT 1`)
698
+ .get(userId);
699
+ const unlabeledPlaceCount = listMovementPlaces([userId]).filter((place) => place.categoryTags.length === 0).length;
700
+ return {
701
+ latestStay: latestStay
702
+ ? {
703
+ id: latestStay.id,
704
+ label: latestStay.label,
705
+ startedAt: latestStay.started_at,
706
+ endedAt: latestStay.ended_at
707
+ }
708
+ : null,
709
+ latestTrip: latestTrip
710
+ ? {
711
+ id: latestTrip.id,
712
+ label: latestTrip.label,
713
+ startedAt: latestTrip.started_at,
714
+ endedAt: latestTrip.ended_at
715
+ }
716
+ : null,
717
+ unlabeledPlaceCount
718
+ };
719
+ }
720
+ function buildSyncSnapshot(pairing) {
721
+ const queuedCaptureCount = getDatabase()
722
+ .prepare(`SELECT COUNT(*) AS count
723
+ FROM watch_capture_events
724
+ WHERE user_id = ?`)
725
+ .get(pairing.user_id);
726
+ const actionReceiptCount = getDatabase()
727
+ .prepare(`SELECT COUNT(*) AS count
728
+ FROM watch_action_receipts
729
+ WHERE user_id = ?`)
730
+ .get(pairing.user_id);
731
+ return {
732
+ pairingSessionId: pairing.id,
733
+ generatedAt: nowIso(),
734
+ storedCaptureCount: queuedCaptureCount.count,
735
+ actionReceiptCount: actionReceiptCount.count
736
+ };
737
+ }
738
+ function buildWatchSurfaces() {
739
+ return [
740
+ { id: "now", title: "Now", icon: "sparkle" },
741
+ { id: "work", title: "Work", icon: "kanban" },
742
+ { id: "habits", title: "Habits", icon: "habit" },
743
+ { id: "goals", title: "Goals", icon: "scope" },
744
+ { id: "today", title: "Today", icon: "calendar" },
745
+ { id: "health", title: "Health", icon: "heart" },
746
+ { id: "movement", title: "Move", icon: "location" },
747
+ { id: "psyche", title: "Psyche", icon: "mind" },
748
+ { id: "inbox", title: "Inbox", icon: "tray" },
749
+ { id: "sync", title: "Sync", icon: "antenna" }
750
+ ];
751
+ }
480
752
  export function buildWatchBootstrap(pairing, options) {
481
753
  assertWatchReady(pairing);
482
754
  const habits = listHabits({ status: "active", limit: 64 })
@@ -511,8 +783,37 @@ export function buildWatchBootstrap(pairing, options) {
511
783
  last7History: history
512
784
  };
513
785
  });
786
+ const pendingPrompts = buildPendingPrompts(pairing.user_id);
787
+ const work = buildWorkSnapshot(pairing);
788
+ const direction = buildDirectionSnapshot(pairing);
789
+ const today = buildTodaySnapshot(pairing);
790
+ const generatedAt = nowIso();
514
791
  return {
515
- generatedAt: nowIso(),
792
+ schemaVersion: 2,
793
+ generatedAt,
794
+ surfaces: buildWatchSurfaces(),
795
+ now: {
796
+ currentRun: work.currentRun,
797
+ nextTask: work.nextTask,
798
+ dueHabitCount: habits.filter((habit) => habit.dueToday).length,
799
+ pendingPromptCount: pendingPrompts.length,
800
+ generatedAt
801
+ },
802
+ work,
803
+ goals: direction.goals,
804
+ projects: direction.projects,
805
+ today,
806
+ health: buildHealthSnapshot(pairing.user_id),
807
+ movement: buildMovementSnapshot(pairing.user_id),
808
+ psyche: {
809
+ emotionOptions,
810
+ triggerOptions,
811
+ routinePromptOptions
812
+ },
813
+ inbox: {
814
+ prompts: pendingPrompts
815
+ },
816
+ sync: buildSyncSnapshot(pairing),
516
817
  habits,
517
818
  checkInOptions: {
518
819
  activities: activityOptions,
@@ -522,7 +823,7 @@ export function buildWatchBootstrap(pairing, options) {
522
823
  routinePrompts: routinePromptOptions,
523
824
  recentPeople: recentPeopleLabels(pairing.user_id)
524
825
  },
525
- pendingPrompts: buildPendingPrompts(pairing.user_id)
826
+ pendingPrompts
526
827
  };
527
828
  }
528
829
  export function ingestWatchCaptureBatch(pairing, input) {
@@ -573,3 +874,262 @@ export function ingestWatchCaptureBatch(pairing, input) {
573
874
  };
574
875
  });
575
876
  }
877
+ function readActionReceipt(userId, actionId) {
878
+ return getDatabase()
879
+ .prepare(`SELECT action_id, kind, processed_at, status, result_json, error_json
880
+ FROM watch_action_receipts
881
+ WHERE user_id = ? AND action_id = ?`)
882
+ .get(userId, actionId);
883
+ }
884
+ function writeActionReceipt(pairing, command, receipt) {
885
+ const processedAt = nowIso();
886
+ getDatabase()
887
+ .prepare(`INSERT INTO watch_action_receipts (
888
+ id, pairing_session_id, user_id, action_id, kind, received_at,
889
+ processed_at, status, result_json, error_json, created_at
890
+ )
891
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
892
+ ON CONFLICT(user_id, action_id) DO NOTHING`)
893
+ .run(`watchact_${randomUUID().replaceAll("-", "").slice(0, 12)}`, pairing.id, pairing.user_id, command.id, command.kind, command.createdAt, processedAt, receipt.status === "failed" ? "failed" : "processed", JSON.stringify(receipt.result), JSON.stringify(receipt.error ?? {}), processedAt);
894
+ return {
895
+ actionId: command.id,
896
+ kind: command.kind,
897
+ processedAt,
898
+ ...receipt
899
+ };
900
+ }
901
+ const habitCommandPayloadSchema = z.object({
902
+ habitId: z.string().trim().min(1),
903
+ dateKey: z.string().trim().min(1).default(() => formatLocalDateKey()),
904
+ status: z.enum(["done", "missed"]),
905
+ note: z.string().trim().default("")
906
+ });
907
+ const captureCommandPayloadSchema = z.object({
908
+ eventType: watchCaptureEventTypeSchema,
909
+ recordedAt: z.string().datetime().default(() => nowIso()),
910
+ promptId: z.string().trim().min(1).nullable().optional().default(null),
911
+ linkedContext: watchLinkedContextSchema.default({}),
912
+ payload: z.record(z.string(), z.unknown()).default({})
913
+ });
914
+ const taskRunStartCommandPayloadSchema = z.object({
915
+ taskId: z.string().trim().min(1),
916
+ actor: z.string().trim().min(1).optional(),
917
+ timerMode: z.enum(["planned", "unlimited"]).default("unlimited"),
918
+ plannedDurationSeconds: z
919
+ .number()
920
+ .int()
921
+ .min(60)
922
+ .max(86_400)
923
+ .nullable()
924
+ .default(null),
925
+ isCurrent: z.boolean().default(true),
926
+ leaseTtlSeconds: z.number().int().min(1).max(14_400).default(900),
927
+ note: z.string().trim().default(""),
928
+ overrideReason: z.string().trim().optional()
929
+ });
930
+ const taskRunIdCommandPayloadSchema = z.object({
931
+ runId: z.string().trim().min(1),
932
+ actor: z.string().trim().min(1).optional(),
933
+ leaseTtlSeconds: z.number().int().min(1).max(14_400).default(900),
934
+ note: z.string().trim().default(""),
935
+ overrideReason: z.string().trim().optional()
936
+ });
937
+ const taskStatusCommandPayloadSchema = z.object({
938
+ taskId: z.string().trim().min(1),
939
+ status: z.enum(["backlog", "focus", "in_progress", "blocked", "done"]),
940
+ note: z.string().trim().default("")
941
+ });
942
+ function processWatchCommand(pairing, command) {
943
+ const actor = watchActorLabel(pairing);
944
+ switch (command.kind) {
945
+ case "habit_check_in": {
946
+ const payload = habitCommandPayloadSchema.parse(command.payload);
947
+ const habit = createHabitCheckIn(payload.habitId, {
948
+ dateKey: payload.dateKey,
949
+ status: payload.status,
950
+ note: payload.note
951
+ }, { source: "system", actor: `watch:${command.id}` });
952
+ if (!habit) {
953
+ throw new HttpError(404, "watch_habit_not_found", "Habit not found");
954
+ }
955
+ return { habitId: habit.id, status: payload.status };
956
+ }
957
+ case "capture_event": {
958
+ const rawPayload = command.payload;
959
+ const payload = captureCommandPayloadSchema.parse({
960
+ ...rawPayload,
961
+ linkedContext: typeof rawPayload.linkedContext === "object" &&
962
+ rawPayload.linkedContext != null &&
963
+ Array.isArray(rawPayload.linkedContext) === false
964
+ ? rawPayload.linkedContext
965
+ : {
966
+ placeId: rawPayload.placeId,
967
+ stayId: rawPayload.stayId,
968
+ tripId: rawPayload.tripId,
969
+ workoutId: rawPayload.workoutId
970
+ }
971
+ });
972
+ return {
973
+ receipt: ingestWatchCaptureBatch(pairing, {
974
+ sessionId: pairing.id,
975
+ pairingToken: "watch-command",
976
+ device: {
977
+ name: "Apple Watch",
978
+ platform: "watchos",
979
+ appVersion: "",
980
+ sourceDevice: "Apple Watch"
981
+ },
982
+ events: [
983
+ {
984
+ dedupeKey: command.id,
985
+ eventType: payload.eventType,
986
+ recordedAt: payload.recordedAt,
987
+ promptId: payload.promptId,
988
+ linkedContext: payload.linkedContext,
989
+ payload: payload.payload
990
+ }
991
+ ]
992
+ })
993
+ };
994
+ }
995
+ case "task_run_start": {
996
+ const payload = taskRunStartCommandPayloadSchema.parse(command.payload);
997
+ const result = claimTaskRun(payload.taskId, {
998
+ actor: payload.actor ?? actor,
999
+ timerMode: payload.timerMode,
1000
+ plannedDurationSeconds: payload.plannedDurationSeconds,
1001
+ isCurrent: payload.isCurrent,
1002
+ leaseTtlSeconds: payload.leaseTtlSeconds,
1003
+ note: payload.note,
1004
+ overrideReason: payload.overrideReason
1005
+ }, new Date(), { source: "system" });
1006
+ return { taskRun: result.run, replayed: result.replayed };
1007
+ }
1008
+ case "task_run_heartbeat": {
1009
+ const payload = taskRunIdCommandPayloadSchema.parse(command.payload);
1010
+ return {
1011
+ taskRun: heartbeatTaskRun(payload.runId, {
1012
+ actor: payload.actor ?? actor,
1013
+ leaseTtlSeconds: payload.leaseTtlSeconds,
1014
+ note: payload.note,
1015
+ overrideReason: payload.overrideReason
1016
+ }, new Date(), { source: "system" })
1017
+ };
1018
+ }
1019
+ case "task_run_focus": {
1020
+ const payload = taskRunIdCommandPayloadSchema.parse(command.payload);
1021
+ return {
1022
+ taskRun: focusTaskRun(payload.runId, { actor: payload.actor ?? actor }, new Date(), { source: "system" })
1023
+ };
1024
+ }
1025
+ case "task_run_complete": {
1026
+ const payload = taskRunIdCommandPayloadSchema.parse(command.payload);
1027
+ return {
1028
+ taskRun: completeTaskRun(payload.runId, { actor: payload.actor ?? actor, note: payload.note }, new Date(), { source: "system" })
1029
+ };
1030
+ }
1031
+ case "task_run_release": {
1032
+ const payload = taskRunIdCommandPayloadSchema.parse(command.payload);
1033
+ return {
1034
+ taskRun: releaseTaskRun(payload.runId, { actor: payload.actor ?? actor, note: payload.note }, new Date(), { source: "system" })
1035
+ };
1036
+ }
1037
+ case "task_status_update": {
1038
+ const payload = taskStatusCommandPayloadSchema.parse(command.payload);
1039
+ const task = updateTask(payload.taskId, { status: payload.status }, { source: "system", actor: actor });
1040
+ if (!task) {
1041
+ throw new HttpError(404, "watch_task_not_found", "Task not found");
1042
+ }
1043
+ if (payload.note.length > 0) {
1044
+ ingestWatchCaptureBatch(pairing, {
1045
+ sessionId: pairing.id,
1046
+ pairingToken: "watch-command",
1047
+ device: {
1048
+ name: "Apple Watch",
1049
+ platform: "watchos",
1050
+ appVersion: "",
1051
+ sourceDevice: "Apple Watch"
1052
+ },
1053
+ events: [
1054
+ {
1055
+ dedupeKey: `${command.id}:note`,
1056
+ eventType: "dictated_note",
1057
+ promptId: null,
1058
+ recordedAt: command.createdAt,
1059
+ linkedContext: {},
1060
+ payload: { note: payload.note, taskId: payload.taskId }
1061
+ }
1062
+ ]
1063
+ });
1064
+ }
1065
+ return { taskId: task.id, status: task.status };
1066
+ }
1067
+ }
1068
+ }
1069
+ function commandErrorPayload(error) {
1070
+ if (error instanceof HttpError) {
1071
+ return {
1072
+ statusCode: error.statusCode,
1073
+ code: error.code,
1074
+ message: error.message,
1075
+ details: error.details ?? null
1076
+ };
1077
+ }
1078
+ if (error instanceof ZodError) {
1079
+ return {
1080
+ statusCode: 400,
1081
+ code: "watch_command_validation_failed",
1082
+ message: "Watch command validation failed",
1083
+ issues: error.issues
1084
+ };
1085
+ }
1086
+ return {
1087
+ statusCode: 500,
1088
+ code: "watch_command_failed",
1089
+ message: error instanceof Error ? error.message : "Unknown watch command error"
1090
+ };
1091
+ }
1092
+ export function ingestWatchCommandBatch(pairing, input) {
1093
+ assertWatchReady(pairing);
1094
+ const parsed = mobileWatchCommandBatchSchema.parse(input);
1095
+ const receipts = [];
1096
+ for (const command of parsed.commands) {
1097
+ const existing = readActionReceipt(pairing.user_id, command.id);
1098
+ if (existing) {
1099
+ receipts.push({
1100
+ actionId: existing.action_id,
1101
+ kind: existing.kind,
1102
+ status: "replayed",
1103
+ processedAt: existing.processed_at,
1104
+ result: safeJsonParse(existing.result_json, {}),
1105
+ error: safeJsonParse(existing.error_json, {})
1106
+ });
1107
+ continue;
1108
+ }
1109
+ try {
1110
+ const result = processWatchCommand(pairing, command);
1111
+ receipts.push(writeActionReceipt(pairing, command, {
1112
+ status: "processed",
1113
+ result
1114
+ }));
1115
+ }
1116
+ catch (error) {
1117
+ const errorPayload = commandErrorPayload(error);
1118
+ receipts.push(writeActionReceipt(pairing, command, {
1119
+ status: "failed",
1120
+ result: {},
1121
+ error: errorPayload
1122
+ }));
1123
+ }
1124
+ }
1125
+ return {
1126
+ receivedCount: parsed.commands.length,
1127
+ processedCount: receipts.filter((receipt) => receipt.status === "processed")
1128
+ .length,
1129
+ replayedCount: receipts.filter((receipt) => receipt.status === "replayed")
1130
+ .length,
1131
+ failedCount: receipts.filter((receipt) => receipt.status === "failed")
1132
+ .length,
1133
+ receipts
1134
+ };
1135
+ }
@@ -143,6 +143,14 @@ function copyProxyHeaders(response, reply) {
143
143
  reply.header(name, value);
144
144
  }
145
145
  }
146
+ function isHtmlResponse(contentType) {
147
+ const values = Array.isArray(contentType) ? contentType : [contentType];
148
+ return values.some((value) => typeof value === "string" && value.toLowerCase().includes("text/html"));
149
+ }
150
+ function forceUncachedHtml(reply) {
151
+ reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
152
+ reply.header("Pragma", "no-cache");
153
+ }
146
154
  const hopByHopHeaders = new Set([
147
155
  "connection",
148
156
  "content-length",
@@ -164,7 +172,10 @@ async function proxyDevAsset(input) {
164
172
  const response = await input.fetchImpl(target, { redirect: "manual" });
165
173
  input.reply.code(response.status);
166
174
  copyProxyHeaders(response, input.reply);
167
- if (!response.headers.has("cache-control")) {
175
+ if (isHtmlResponse(response.headers.get("content-type") ?? undefined)) {
176
+ forceUncachedHtml(input.reply);
177
+ }
178
+ else if (!response.headers.has("cache-control")) {
168
179
  input.reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
169
180
  }
170
181
  if (!response.body) {
@@ -205,7 +216,10 @@ export function createKeepAliveDevAssetProxy() {
205
216
  }
206
217
  input.reply.header(name, value);
207
218
  }
208
- if (!response.headers["cache-control"]) {
219
+ if (isHtmlResponse(response.headers["content-type"])) {
220
+ forceUncachedHtml(input.reply);
221
+ }
222
+ else if (!response.headers["cache-control"]) {
209
223
  input.reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
210
224
  }
211
225
  const chunks = [];
@@ -444,7 +458,7 @@ async function serveAsset(requestPath, reply, options) {
444
458
  reply.type(contentTypes[ext] ?? "application/octet-stream");
445
459
  reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
446
460
  if (ext === ".html") {
447
- reply.header("Pragma", "no-cache");
461
+ forceUncachedHtml(reply);
448
462
  }
449
463
  return payload;
450
464
  }
@@ -453,8 +467,7 @@ async function serveAsset(requestPath, reply, options) {
453
467
  try {
454
468
  const payload = await readFile(path.join(clientDir, "index.html"));
455
469
  reply.type(contentTypes[".html"]);
456
- reply.header("Cache-Control", "no-store, max-age=0, must-revalidate");
457
- reply.header("Pragma", "no-cache");
470
+ forceUncachedHtml(reply);
458
471
  return payload;
459
472
  }
460
473
  catch {