forge-openclaw-plugin 0.2.23 → 0.2.25

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 (84) hide show
  1. package/README.md +13 -0
  2. package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
  3. package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
  4. package/dist/assets/index-CFCKDIMH.js +67 -0
  5. package/dist/assets/index-CFCKDIMH.js.map +1 -0
  6. package/dist/assets/index-ZPY6U1TU.css +1 -0
  7. package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
  8. package/dist/assets/motion-DvkU14p-.js.map +1 -0
  9. package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
  10. package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
  11. package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
  12. package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
  13. package/dist/assets/vendor-D9PTEPSB.js +824 -0
  14. package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
  15. package/dist/assets/viz-Cqb6s--o.js +34 -0
  16. package/dist/assets/viz-Cqb6s--o.js.map +1 -0
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.d.ts +1 -1
  19. package/dist/openclaw/parity.js +29 -0
  20. package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
  21. package/dist/openclaw/plugin-entry-shared.js +7 -4
  22. package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
  23. package/dist/openclaw/routes.js +236 -0
  24. package/dist/openclaw/session-bootstrap.d.ts +78 -0
  25. package/dist/openclaw/session-bootstrap.js +240 -0
  26. package/dist/openclaw/tools.js +279 -3
  27. package/dist/server/app.js +855 -19
  28. package/dist/server/connectors/box-registry.js +257 -0
  29. package/dist/server/db.js +2 -0
  30. package/dist/server/discovery-advertiser.js +114 -0
  31. package/dist/server/health.js +39 -11
  32. package/dist/server/index.js +4 -0
  33. package/dist/server/managers/platform/llm-manager.js +40 -4
  34. package/dist/server/managers/platform/openai-responses-provider.js +129 -19
  35. package/dist/server/movement.js +2935 -0
  36. package/dist/server/openapi.js +628 -5
  37. package/dist/server/psyche-types.js +15 -1
  38. package/dist/server/questionnaire-flow.js +552 -0
  39. package/dist/server/questionnaire-seeds.js +853 -0
  40. package/dist/server/questionnaire-types.js +340 -0
  41. package/dist/server/repositories/ai-connectors.js +944 -0
  42. package/dist/server/repositories/ai-processors.js +547 -0
  43. package/dist/server/repositories/diagnostic-logs.js +57 -4
  44. package/dist/server/repositories/entity-ownership.js +9 -1
  45. package/dist/server/repositories/habits.js +77 -9
  46. package/dist/server/repositories/model-settings.js +216 -0
  47. package/dist/server/repositories/notes.js +57 -15
  48. package/dist/server/repositories/preferences.js +124 -0
  49. package/dist/server/repositories/questionnaires.js +1338 -0
  50. package/dist/server/repositories/rewards.js +2 -2
  51. package/dist/server/repositories/settings.js +108 -12
  52. package/dist/server/repositories/surface-layouts.js +76 -0
  53. package/dist/server/repositories/wiki-memory.js +5 -1
  54. package/dist/server/services/entity-crud.js +81 -2
  55. package/dist/server/services/openai-codex-oauth.js +153 -0
  56. package/dist/server/services/psyche-observation-calendar.js +46 -0
  57. package/dist/server/types.js +492 -3
  58. package/dist/server/watch-mobile.js +562 -0
  59. package/dist/server/web.js +9 -2
  60. package/openclaw.plugin.json +1 -1
  61. package/package.json +6 -1
  62. package/server/migrations/024_questionnaires.sql +96 -0
  63. package/server/migrations/025_ai_model_connections.sql +26 -0
  64. package/server/migrations/026_custom_theme_settings.sql +2 -0
  65. package/server/migrations/027_ai_processors.sql +31 -0
  66. package/server/migrations/028_movement_domain.sql +136 -0
  67. package/server/migrations/029_watch_micro_capture.sql +23 -0
  68. package/server/migrations/030_surface_layouts.sql +5 -0
  69. package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  70. package/server/migrations/032_ai_connectors.sql +44 -0
  71. package/server/migrations/033_movement_trip_point_sync.sql +36 -0
  72. package/server/migrations/034_movement_segment_sync.sql +49 -0
  73. package/skills/forge-openclaw/SKILL.md +12 -1
  74. package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
  75. package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
  76. package/dist/assets/index-Ch_xeZ2u.js +0 -63
  77. package/dist/assets/index-Ch_xeZ2u.js.map +0 -1
  78. package/dist/assets/index-DvVM7K6j.css +0 -1
  79. package/dist/assets/motion-D4sZgCHd.js.map +0 -1
  80. package/dist/assets/vendor-De38P6YR.js +0 -729
  81. package/dist/assets/vendor-De38P6YR.js.map +0 -1
  82. package/dist/assets/viz-C6hfyqzu.js +0 -34
  83. package/dist/assets/viz-C6hfyqzu.js.map +0 -1
  84. package/skills/forge-openclaw/cron_jobs.md +0 -395
@@ -0,0 +1,562 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { getDatabase, runInTransaction } from "./db.js";
4
+ import { HttpError } from "./errors.js";
5
+ import { updateWorkoutMetadata } from "./health.js";
6
+ import { canonicalizeMovementCategoryTags, listMovementPlaces, normalizeMovementCategoryTag, updateMovementPlace } from "./movement.js";
7
+ import { listHabits } from "./repositories/habits.js";
8
+ const watchCapability = "watch-ready";
9
+ const watchHistoryStateSchema = z.enum(["aligned", "unaligned", "unknown"]);
10
+ const watchPromptKindSchema = z.enum([
11
+ "new_place",
12
+ "trip_label",
13
+ "workout_annotation",
14
+ "social_follow_up",
15
+ "unknown_block",
16
+ "routine_check"
17
+ ]);
18
+ const watchCaptureEventTypeSchema = z.enum([
19
+ "activity_check_in",
20
+ "emotion_check_in",
21
+ "mark_moment",
22
+ "trigger_capture",
23
+ "place_label",
24
+ "trip_label",
25
+ "social_context",
26
+ "workout_annotation",
27
+ "routine_check",
28
+ "dictated_note",
29
+ "retrospective_label"
30
+ ]);
31
+ const watchDeviceSchema = z.object({
32
+ name: z.string().trim().default("Apple Watch"),
33
+ platform: z.string().trim().default("watchos"),
34
+ appVersion: z.string().trim().default(""),
35
+ sourceDevice: z.string().trim().default("Apple Watch")
36
+ });
37
+ const watchLinkedContextSchema = z.object({
38
+ placeId: z.string().trim().min(1).optional(),
39
+ stayId: z.string().trim().min(1).optional(),
40
+ tripId: z.string().trim().min(1).optional(),
41
+ workoutId: z.string().trim().min(1).optional()
42
+ });
43
+ const watchCaptureEventSchema = z.object({
44
+ dedupeKey: z.string().trim().min(1),
45
+ eventType: watchCaptureEventTypeSchema,
46
+ recordedAt: z.string().datetime(),
47
+ promptId: z.string().trim().min(1).nullable().optional().default(null),
48
+ linkedContext: watchLinkedContextSchema.default({}),
49
+ payload: z.record(z.string(), z.unknown()).default({})
50
+ });
51
+ export const mobileWatchBootstrapSchema = z.object({
52
+ sessionId: z.string().trim().min(1),
53
+ pairingToken: z.string().trim().min(1)
54
+ });
55
+ export const mobileWatchHabitCheckInSchema = z.object({
56
+ sessionId: z.string().trim().min(1),
57
+ pairingToken: z.string().trim().min(1),
58
+ dedupeKey: z.string().trim().min(1),
59
+ dateKey: z.string().trim().min(1).default(new Date().toISOString().slice(0, 10)),
60
+ status: z.enum(["done", "missed"]),
61
+ note: z.string().trim().default("")
62
+ });
63
+ export const mobileWatchCaptureBatchSchema = z.object({
64
+ sessionId: z.string().trim().min(1),
65
+ pairingToken: z.string().trim().min(1),
66
+ device: watchDeviceSchema.default({}),
67
+ events: z.array(watchCaptureEventSchema).max(100).default([])
68
+ });
69
+ function safeJsonParse(raw, fallback) {
70
+ if (!raw || raw.trim().length === 0) {
71
+ return fallback;
72
+ }
73
+ try {
74
+ return JSON.parse(raw);
75
+ }
76
+ catch {
77
+ return fallback;
78
+ }
79
+ }
80
+ function nowIso() {
81
+ return new Date().toISOString();
82
+ }
83
+ function formatDateKey(date) {
84
+ return date.toISOString().slice(0, 10);
85
+ }
86
+ function parseDateKey(dateKey) {
87
+ const [year, month, day] = dateKey.split("-").map(Number);
88
+ return new Date(Date.UTC(year, month - 1, day));
89
+ }
90
+ function startOfUtcDay(date) {
91
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
92
+ }
93
+ function addUtcDays(date, days) {
94
+ const next = new Date(date);
95
+ next.setUTCDate(next.getUTCDate() + days);
96
+ return next;
97
+ }
98
+ function startOfUtcWeek(date) {
99
+ const start = startOfUtcDay(date);
100
+ const offset = (start.getUTCDay() + 6) % 7;
101
+ start.setUTCDate(start.getUTCDate() - offset);
102
+ return start;
103
+ }
104
+ function isAlignedCheckIn(habit, status) {
105
+ return ((habit.polarity === "positive" && status === "done") ||
106
+ (habit.polarity === "negative" && status === "missed"));
107
+ }
108
+ function alignedActionLabel(polarity) {
109
+ return polarity === "positive" ? "Done" : "Resisted";
110
+ }
111
+ function unalignedActionLabel(polarity) {
112
+ return polarity === "positive" ? "Missed" : "Performed";
113
+ }
114
+ function formatCadenceLabel(habit) {
115
+ if (habit.frequency === "daily") {
116
+ return `${habit.targetCount}x daily`;
117
+ }
118
+ const weekdayLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
119
+ const labels = habit.weekDays.map((day) => weekdayLabels[day]).join(", ");
120
+ return `${habit.targetCount}x weekly${labels ? ` · ${labels}` : ""}`;
121
+ }
122
+ function buildHabitHistory(habit, options) {
123
+ const now = options?.anchorDateKey
124
+ ? parseDateKey(options.anchorDateKey)
125
+ : new Date();
126
+ if (habit.frequency === "daily") {
127
+ const today = startOfUtcDay(now);
128
+ return Array.from({ length: 7 }, (_, index) => {
129
+ const offset = index - 6;
130
+ const date = addUtcDays(today, offset);
131
+ const dateKey = formatDateKey(date);
132
+ const checkIn = habit.checkIns.find((entry) => entry.dateKey === dateKey) ?? null;
133
+ return {
134
+ id: dateKey,
135
+ label: ["S", "M", "T", "W", "T", "F", "S"][date.getUTCDay()],
136
+ periodKey: dateKey,
137
+ current: offset === 0,
138
+ state: checkIn
139
+ ? isAlignedCheckIn(habit, checkIn.status)
140
+ ? "aligned"
141
+ : "unaligned"
142
+ : "unknown"
143
+ };
144
+ });
145
+ }
146
+ const thisWeek = startOfUtcWeek(now);
147
+ return Array.from({ length: 7 }, (_, index) => {
148
+ const offset = index - 6;
149
+ const weekStart = addUtcDays(thisWeek, offset * 7);
150
+ const weekKey = formatDateKey(weekStart);
151
+ const weekEntries = habit.checkIns.filter((entry) => {
152
+ const entryWeek = formatDateKey(startOfUtcWeek(parseDateKey(entry.dateKey)));
153
+ return entryWeek === weekKey;
154
+ });
155
+ const alignedCount = weekEntries.filter((entry) => isAlignedCheckIn(habit, entry.status)).length;
156
+ const unalignedCount = weekEntries.length - alignedCount;
157
+ return {
158
+ id: weekKey,
159
+ label: offset === 0 ? "Now" : `${Math.abs(offset)}w`,
160
+ periodKey: weekKey,
161
+ current: offset === 0,
162
+ state: weekEntries.length === 0
163
+ ? "unknown"
164
+ : alignedCount >= unalignedCount
165
+ ? "aligned"
166
+ : "unaligned"
167
+ };
168
+ });
169
+ }
170
+ const activityOptions = [
171
+ "Working",
172
+ "Coding",
173
+ "Admin",
174
+ "Reading",
175
+ "Commuting",
176
+ "Walking",
177
+ "Eating",
178
+ "Socializing",
179
+ "Resting",
180
+ "Training",
181
+ "Shopping"
182
+ ];
183
+ const emotionOptions = [
184
+ "Calm",
185
+ "Focused",
186
+ "Content",
187
+ "Energized",
188
+ "Tired",
189
+ "Restless",
190
+ "Low",
191
+ "Tense",
192
+ "Anxious",
193
+ "Overwhelmed",
194
+ "Relieved"
195
+ ];
196
+ const triggerOptions = [
197
+ "Conflict",
198
+ "Pleasant moment",
199
+ "Urge",
200
+ "Avoidance",
201
+ "Social exposure",
202
+ "Setback",
203
+ "Breakthrough",
204
+ "Rumination",
205
+ "Shame spike",
206
+ "Victory"
207
+ ];
208
+ const placeCategoryOptions = [
209
+ "Home",
210
+ "Work",
211
+ "Clinic",
212
+ "Grocery",
213
+ "Gym",
214
+ "Cafe",
215
+ "Nature",
216
+ "Travel",
217
+ "Social",
218
+ "Other"
219
+ ];
220
+ const routinePromptOptions = [
221
+ "Medication taken?",
222
+ "Caffeine?",
223
+ "Meal?",
224
+ "Sunlight exposure?",
225
+ "Wind-down started?"
226
+ ];
227
+ const watchCategoryMap = new Map([
228
+ ["Home", ["home"]],
229
+ ["Work", ["workplace"]],
230
+ ["Clinic", ["clinic"]],
231
+ ["Grocery", ["grocery"]],
232
+ ["Gym", ["gym"]],
233
+ ["Cafe", ["cafe"]],
234
+ ["Nature", ["nature"]],
235
+ ["Travel", ["travel"]],
236
+ ["Social", ["social"]],
237
+ ["Other", ["other"]]
238
+ ]);
239
+ function recentPeopleLabels(userId) {
240
+ const labels = new Set();
241
+ const sources = [
242
+ ...getDatabase()
243
+ .prepare(`SELECT linked_people_json
244
+ FROM movement_places
245
+ WHERE user_id = ?
246
+ ORDER BY updated_at DESC
247
+ LIMIT 25`)
248
+ .all(userId),
249
+ ...getDatabase()
250
+ .prepare(`SELECT linked_people_json
251
+ FROM movement_trips
252
+ WHERE user_id = ?
253
+ ORDER BY started_at DESC
254
+ LIMIT 25`)
255
+ .all(userId)
256
+ ];
257
+ for (const source of sources) {
258
+ const people = safeJsonParse(source.linked_people_json, []);
259
+ for (const person of people) {
260
+ const label = person.label?.trim();
261
+ if (label) {
262
+ labels.add(label);
263
+ }
264
+ }
265
+ }
266
+ const captureRows = getDatabase()
267
+ .prepare(`SELECT payload_json
268
+ FROM watch_capture_events
269
+ WHERE user_id = ?
270
+ AND event_type = 'social_context'
271
+ ORDER BY recorded_at DESC
272
+ LIMIT 25`)
273
+ .all(userId);
274
+ for (const row of captureRows) {
275
+ const payload = safeJsonParse(row.payload_json, {});
276
+ const label = typeof payload.personLabel === "string" ? payload.personLabel.trim() : "";
277
+ if (label) {
278
+ labels.add(label);
279
+ }
280
+ }
281
+ return [...labels].slice(0, 8);
282
+ }
283
+ function buildPendingPrompts(userId) {
284
+ const prompts = [];
285
+ const unlabeledPlaces = listMovementPlaces([userId]).filter((place) => place.categoryTags.length === 0);
286
+ for (const place of unlabeledPlaces.slice(0, 2)) {
287
+ prompts.push({
288
+ id: `prompt_place_${place.id}`,
289
+ kind: "new_place",
290
+ title: "New place detected",
291
+ message: `What is ${place.label || "this place"}?`,
292
+ createdAt: nowIso(),
293
+ linkedContext: { placeId: place.id },
294
+ choices: placeCategoryOptions.slice(0, 6)
295
+ });
296
+ }
297
+ const trips = getDatabase()
298
+ .prepare(`SELECT id, label, started_at, tags_json, metadata_json
299
+ FROM movement_trips
300
+ WHERE user_id = ?
301
+ ORDER BY started_at DESC
302
+ LIMIT 3`)
303
+ .all(userId);
304
+ for (const trip of trips) {
305
+ const tags = safeJsonParse(trip.tags_json, []);
306
+ if (tags.length === 0) {
307
+ prompts.push({
308
+ id: `prompt_trip_${trip.id}`,
309
+ kind: "trip_label",
310
+ title: "Label this trip",
311
+ message: trip.label || "What was this trip for?",
312
+ createdAt: trip.started_at,
313
+ linkedContext: { tripId: trip.id },
314
+ choices: ["Work", "Groceries", "Gym", "Social", "Nature", "Errand"]
315
+ });
316
+ }
317
+ }
318
+ const workouts = getDatabase()
319
+ .prepare(`SELECT id, workout_type, started_at, mood_after, meaning_text, subjective_effort
320
+ FROM health_workout_sessions
321
+ WHERE user_id = ?
322
+ ORDER BY started_at DESC
323
+ LIMIT 3`)
324
+ .all(userId);
325
+ for (const workout of workouts) {
326
+ if (workout.subjective_effort == null &&
327
+ workout.mood_after.trim().length === 0 &&
328
+ workout.meaning_text.trim().length === 0) {
329
+ prompts.push({
330
+ id: `prompt_workout_${workout.id}`,
331
+ kind: "workout_annotation",
332
+ title: "How was that workout?",
333
+ message: `Add quick context for your ${workout.workout_type}.`,
334
+ createdAt: workout.started_at,
335
+ linkedContext: { workoutId: workout.id },
336
+ choices: ["Good", "Neutral", "Hard", "Restorative"]
337
+ });
338
+ }
339
+ }
340
+ const stays = getDatabase()
341
+ .prepare(`SELECT id, label, started_at, metadata_json
342
+ FROM movement_stays
343
+ WHERE user_id = ?
344
+ ORDER BY started_at DESC
345
+ LIMIT 3`)
346
+ .all(userId);
347
+ for (const stay of stays) {
348
+ if (stay.label.trim().length === 0) {
349
+ prompts.push({
350
+ id: `prompt_stay_${stay.id}`,
351
+ kind: "unknown_block",
352
+ title: "Unknown block",
353
+ message: "Want to label this block before it fades?",
354
+ createdAt: stay.started_at,
355
+ linkedContext: { stayId: stay.id },
356
+ choices: ["Work", "Social", "Errand", "Nature", "Rest"]
357
+ });
358
+ }
359
+ }
360
+ prompts.push({
361
+ id: "prompt_routine_evening",
362
+ kind: "routine_check",
363
+ title: "Quick routine check",
364
+ message: "Capture one routine signal before it gets lost.",
365
+ createdAt: nowIso(),
366
+ linkedContext: {},
367
+ choices: routinePromptOptions.slice(0, 4)
368
+ });
369
+ return prompts.slice(0, 8);
370
+ }
371
+ function projectionForStoredEvent(event) {
372
+ if (event.eventType === "place_label" && event.linkedContext.placeId) {
373
+ const nextLabel = typeof event.payload.label === "string" ? event.payload.label.trim() : "";
374
+ const categoryCandidate = Array.isArray(event.payload.categoryTags)
375
+ ? event.payload.categoryTags
376
+ : typeof event.payload.category === "string"
377
+ ? watchCategoryMap.get(event.payload.category) ?? [event.payload.category]
378
+ : [];
379
+ const categoryTags = canonicalizeMovementCategoryTags(categoryCandidate.flatMap((value) => typeof value === "string" ? [normalizeMovementCategoryTag(value)] : []));
380
+ try {
381
+ const place = updateMovementPlace(event.linkedContext.placeId, {
382
+ ...(nextLabel ? { label: nextLabel } : {}),
383
+ ...(categoryTags.length > 0 ? { categoryTags } : {})
384
+ }, { source: "system", actor: null });
385
+ if (!place) {
386
+ return {
387
+ status: "projection_failed",
388
+ details: { reason: "place_not_found" }
389
+ };
390
+ }
391
+ return {
392
+ status: "projected",
393
+ details: {
394
+ target: "movement_place",
395
+ placeId: place.id,
396
+ categoryTags: place.categoryTags
397
+ }
398
+ };
399
+ }
400
+ catch (error) {
401
+ return {
402
+ status: "projection_failed",
403
+ details: {
404
+ reason: "place_update_failed",
405
+ message: error instanceof Error ? error.message : "Unknown place update error"
406
+ }
407
+ };
408
+ }
409
+ }
410
+ if (event.eventType === "workout_annotation" && event.linkedContext.workoutId) {
411
+ try {
412
+ const workout = updateWorkoutMetadata(event.linkedContext.workoutId, {
413
+ subjectiveEffort: typeof event.payload.subjectiveEffort === "number"
414
+ ? Math.max(1, Math.min(10, Math.round(event.payload.subjectiveEffort)))
415
+ : undefined,
416
+ moodBefore: typeof event.payload.moodBefore === "string"
417
+ ? event.payload.moodBefore.trim()
418
+ : undefined,
419
+ moodAfter: typeof event.payload.moodAfter === "string"
420
+ ? event.payload.moodAfter.trim()
421
+ : undefined,
422
+ meaningText: typeof event.payload.meaningText === "string"
423
+ ? event.payload.meaningText.trim()
424
+ : undefined,
425
+ socialContext: typeof event.payload.socialContext === "string"
426
+ ? event.payload.socialContext.trim()
427
+ : undefined,
428
+ tags: Array.isArray(event.payload.tags)
429
+ ? event.payload.tags.filter((value) => typeof value === "string" && value.trim().length > 0)
430
+ : undefined
431
+ }, { source: "system", actor: null });
432
+ if (!workout) {
433
+ return {
434
+ status: "projection_failed",
435
+ details: { reason: "workout_not_found" }
436
+ };
437
+ }
438
+ return {
439
+ status: "projected",
440
+ details: {
441
+ target: "workout",
442
+ workoutId: workout.id
443
+ }
444
+ };
445
+ }
446
+ catch (error) {
447
+ return {
448
+ status: "projection_failed",
449
+ details: {
450
+ reason: "workout_update_failed",
451
+ message: error instanceof Error ? error.message : "Unknown workout update error"
452
+ }
453
+ };
454
+ }
455
+ }
456
+ return {
457
+ status: "stored",
458
+ details: {}
459
+ };
460
+ }
461
+ export function assertWatchReady(pairing) {
462
+ const capabilities = safeJsonParse(pairing.capability_flags_json, []);
463
+ if (!capabilities.includes(watchCapability)) {
464
+ throw new HttpError(403, "watch_pairing_not_enabled", "This companion pairing is not allowed to serve watch data.");
465
+ }
466
+ }
467
+ export function buildWatchBootstrap(pairing, options) {
468
+ assertWatchReady(pairing);
469
+ const habits = listHabits({ status: "active", limit: 64 })
470
+ .filter((habit) => habit.userId === pairing.user_id || pairing.user_id === "user_operator")
471
+ .sort((left, right) => {
472
+ if (left.dueToday !== right.dueToday) {
473
+ return Number(right.dueToday) - Number(left.dueToday);
474
+ }
475
+ if (left.streakCount !== right.streakCount) {
476
+ return right.streakCount - left.streakCount;
477
+ }
478
+ return left.title.localeCompare(right.title);
479
+ })
480
+ .map((habit) => {
481
+ const history = buildHabitHistory(habit, {
482
+ anchorDateKey: options?.anchorDateKey
483
+ });
484
+ const currentPeriodStatus = history.find((entry) => entry.current)?.state ?? "unknown";
485
+ return {
486
+ id: habit.id,
487
+ title: habit.title,
488
+ polarity: habit.polarity,
489
+ frequency: habit.frequency,
490
+ targetCount: habit.targetCount,
491
+ weekDays: habit.weekDays,
492
+ streakCount: habit.streakCount,
493
+ dueToday: habit.dueToday,
494
+ cadenceLabel: formatCadenceLabel(habit),
495
+ alignedActionLabel: alignedActionLabel(habit.polarity),
496
+ unalignedActionLabel: unalignedActionLabel(habit.polarity),
497
+ currentPeriodStatus,
498
+ last7History: history
499
+ };
500
+ });
501
+ return {
502
+ generatedAt: nowIso(),
503
+ habits,
504
+ checkInOptions: {
505
+ activities: activityOptions,
506
+ emotions: emotionOptions,
507
+ triggers: triggerOptions,
508
+ placeCategories: placeCategoryOptions,
509
+ routinePrompts: routinePromptOptions,
510
+ recentPeople: recentPeopleLabels(pairing.user_id)
511
+ },
512
+ pendingPrompts: buildPendingPrompts(pairing.user_id)
513
+ };
514
+ }
515
+ export function ingestWatchCaptureBatch(pairing, input) {
516
+ assertWatchReady(pairing);
517
+ const parsed = mobileWatchCaptureBatchSchema.parse(input);
518
+ const insert = getDatabase().prepare(`INSERT INTO watch_capture_events (
519
+ id, pairing_session_id, user_id, dedupe_key, source_device, event_type, prompt_id,
520
+ recorded_at, received_at, linked_context_json, payload_json,
521
+ projection_status, projection_details_json, created_at
522
+ )
523
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
524
+ const updateProjection = getDatabase().prepare(`UPDATE watch_capture_events
525
+ SET projection_status = ?, projection_details_json = ?
526
+ WHERE id = ?`);
527
+ const existing = getDatabase().prepare(`SELECT id
528
+ FROM watch_capture_events
529
+ WHERE user_id = ? AND dedupe_key = ?`);
530
+ return runInTransaction(() => {
531
+ let storedCount = 0;
532
+ let duplicateCount = 0;
533
+ let projectedCount = 0;
534
+ let projectionFailedCount = 0;
535
+ for (const event of parsed.events) {
536
+ const duplicate = existing.get(pairing.user_id, event.dedupeKey);
537
+ if (duplicate) {
538
+ duplicateCount += 1;
539
+ continue;
540
+ }
541
+ const id = `watchcap_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
542
+ const receivedAt = nowIso();
543
+ insert.run(id, pairing.id, pairing.user_id, event.dedupeKey, parsed.device.sourceDevice, event.eventType, event.promptId, event.recordedAt, receivedAt, JSON.stringify(event.linkedContext), JSON.stringify(event.payload), "stored", "{}", receivedAt);
544
+ storedCount += 1;
545
+ const projection = projectionForStoredEvent(event);
546
+ updateProjection.run(projection.status, JSON.stringify(projection.details), id);
547
+ if (projection.status === "projected") {
548
+ projectedCount += 1;
549
+ }
550
+ else if (projection.status === "projection_failed") {
551
+ projectionFailedCount += 1;
552
+ }
553
+ }
554
+ return {
555
+ receivedCount: parsed.events.length,
556
+ storedCount,
557
+ duplicateCount,
558
+ projectedCount,
559
+ projectionFailedCount
560
+ };
561
+ });
562
+ }
@@ -1,6 +1,7 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  const distDir = path.join(process.cwd(), "dist");
4
+ const packagedRuntimeDistDir = path.join(process.cwd(), "plugins", "forge-codex", "runtime", "dist");
4
5
  const defaultBasePath = process.env.FORGE_BASE_PATH ?? "/forge/";
5
6
  const contentTypes = {
6
7
  ".css": "text/css; charset=utf-8",
@@ -42,8 +43,14 @@ function resolveAsset(clientDir, requestPath) {
42
43
  return path.join(clientDir, safePath);
43
44
  }
44
45
  async function getClientDir() {
45
- await access(path.join(distDir, "index.html"));
46
- return distDir;
46
+ try {
47
+ await access(path.join(distDir, "index.html"));
48
+ return distDir;
49
+ }
50
+ catch {
51
+ await access(path.join(packagedRuntimeDistDir, "index.html"));
52
+ return packagedRuntimeDistDir;
53
+ }
47
54
  }
48
55
  async function serveAsset(requestPath, reply) {
49
56
  if (requestPath.startsWith("/api")) {
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.23",
5
+ "version": "0.2.25",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,9 +44,14 @@
44
44
  "openclaw": "^2026.3.22"
45
45
  },
46
46
  "dependencies": {
47
+ "@azure/msal-node": "^5.1.2",
47
48
  "@fastify/cors": "^10.0.1",
49
+ "@fastify/multipart": "^9.4.0",
48
50
  "@sinclair/typebox": "^0.34.48",
51
+ "adm-zip": "^0.5.17",
49
52
  "fastify": "^5.2.1",
53
+ "node-ical": "^0.20.1",
54
+ "tsdav": "^2.1.8",
50
55
  "zod": "^3.25.67"
51
56
  },
52
57
  "scripts": {