forge-openclaw-plugin 0.2.24 → 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.
- package/README.md +13 -0
- package/dist/assets/{board-_C6oMy5w.js → board-VmF4FAfr.js} +3 -3
- package/dist/assets/{board-_C6oMy5w.js.map → board-VmF4FAfr.js.map} +1 -1
- package/dist/assets/index-CFCKDIMH.js +67 -0
- package/dist/assets/index-CFCKDIMH.js.map +1 -0
- package/dist/assets/index-ZPY6U1TU.css +1 -0
- package/dist/assets/{motion-D4sZgCHd.js → motion-DvkU14p-.js} +3 -3
- package/dist/assets/motion-DvkU14p-.js.map +1 -0
- package/dist/assets/{table-BWzTaky1.js → table-DgiPof9E.js} +2 -2
- package/dist/assets/{table-BWzTaky1.js.map → table-DgiPof9E.js.map} +1 -1
- package/dist/assets/{ui-BzK4azQb.js → ui-nYfoC0Gq.js} +2 -2
- package/dist/assets/{ui-BzK4azQb.js.map → ui-nYfoC0Gq.js.map} +1 -1
- package/dist/assets/vendor-D9PTEPSB.js +824 -0
- package/dist/assets/vendor-D9PTEPSB.js.map +1 -0
- package/dist/assets/viz-Cqb6s--o.js +34 -0
- package/dist/assets/viz-Cqb6s--o.js.map +1 -0
- package/dist/index.html +8 -8
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -0
- package/dist/openclaw/plugin-entry-shared.d.ts +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -4
- package/dist/openclaw/plugin-sdk-types.d.ts +12 -0
- package/dist/openclaw/routes.js +236 -0
- package/dist/openclaw/session-bootstrap.d.ts +78 -0
- package/dist/openclaw/session-bootstrap.js +240 -0
- package/dist/openclaw/tools.js +279 -3
- package/dist/server/app.js +855 -19
- package/dist/server/connectors/box-registry.js +257 -0
- package/dist/server/db.js +2 -0
- package/dist/server/discovery-advertiser.js +114 -0
- package/dist/server/health.js +39 -11
- package/dist/server/index.js +4 -0
- package/dist/server/managers/platform/llm-manager.js +40 -4
- package/dist/server/managers/platform/openai-responses-provider.js +129 -19
- package/dist/server/movement.js +2935 -0
- package/dist/server/openapi.js +628 -5
- package/dist/server/psyche-types.js +15 -1
- package/dist/server/questionnaire-flow.js +552 -0
- package/dist/server/questionnaire-seeds.js +853 -0
- package/dist/server/questionnaire-types.js +340 -0
- package/dist/server/repositories/ai-connectors.js +944 -0
- package/dist/server/repositories/ai-processors.js +547 -0
- package/dist/server/repositories/entity-ownership.js +9 -1
- package/dist/server/repositories/habits.js +69 -5
- package/dist/server/repositories/model-settings.js +216 -0
- package/dist/server/repositories/notes.js +57 -15
- package/dist/server/repositories/preferences.js +124 -0
- package/dist/server/repositories/questionnaires.js +1338 -0
- package/dist/server/repositories/settings.js +108 -12
- package/dist/server/repositories/surface-layouts.js +76 -0
- package/dist/server/repositories/wiki-memory.js +5 -1
- package/dist/server/services/entity-crud.js +81 -2
- package/dist/server/services/openai-codex-oauth.js +153 -0
- package/dist/server/services/psyche-observation-calendar.js +46 -0
- package/dist/server/types.js +492 -3
- package/dist/server/watch-mobile.js +562 -0
- package/dist/server/web.js +9 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
- package/server/migrations/024_questionnaires.sql +96 -0
- package/server/migrations/025_ai_model_connections.sql +26 -0
- package/server/migrations/026_custom_theme_settings.sql +2 -0
- package/server/migrations/027_ai_processors.sql +31 -0
- package/server/migrations/028_movement_domain.sql +136 -0
- package/server/migrations/029_watch_micro_capture.sql +23 -0
- package/server/migrations/030_surface_layouts.sql +5 -0
- package/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/server/migrations/032_ai_connectors.sql +44 -0
- package/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/server/migrations/034_movement_segment_sync.sql +49 -0
- package/skills/forge-openclaw/SKILL.md +12 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +331 -84
- package/skills/forge-openclaw/psyche_entity_playbooks.md +252 -221
- package/dist/assets/index-DTCwBWAs.js +0 -65
- package/dist/assets/index-DTCwBWAs.js.map +0 -1
- package/dist/assets/index-DttXlAgi.css +0 -1
- package/dist/assets/motion-D4sZgCHd.js.map +0 -1
- package/dist/assets/vendor-De38P6YR.js +0 -729
- package/dist/assets/vendor-De38P6YR.js.map +0 -1
- package/dist/assets/viz-C6hfyqzu.js +0 -34
- package/dist/assets/viz-C6hfyqzu.js.map +0 -1
- 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
|
+
}
|
package/dist/server/web.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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")) {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-openclaw-plugin",
|
|
3
|
-
"version": "0.2.
|
|
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": {
|