forge-openclaw-plugin 0.2.67 → 0.2.69
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/dist/assets/{board-DFNV9VAZ.js → board-BfqxFNiQ.js} +1 -1
- package/dist/assets/index-BfLQnCNZ.js +91 -0
- package/dist/assets/index-DIapFz9v.css +1 -0
- package/dist/assets/{motion-CXdn34ih.js → motion-C0ALlgho.js} +1 -1
- package/dist/assets/{table-CEq3bTDv.js → table-WcMjnJll.js} +1 -1
- package/dist/assets/{ui-g7FaEglG.js → ui-B5I-3U91.js} +1 -1
- package/dist/assets/vendor-B-Lq_OG3.css +1 -0
- package/dist/assets/vendor-C56o26_3.js +2163 -0
- package/dist/index.html +8 -8
- package/dist/server/server/migrations/060_psyche_devrage_metrics.sql +40 -0
- package/dist/server/server/migrations/061_health_workout_raw_evidence.sql +95 -0
- package/dist/server/server/src/app.js +94 -12
- package/dist/server/server/src/health-workout-analytics.js +572 -0
- package/dist/server/server/src/health.js +116 -3
- package/dist/server/server/src/openapi.js +238 -0
- package/dist/server/server/src/psyche-types.js +90 -0
- package/dist/server/server/src/services/devrage.js +412 -0
- package/dist/server/server/src/services/psyche.js +3 -0
- package/dist/server/src/lib/api.js +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/060_psyche_devrage_metrics.sql +40 -0
- package/server/migrations/061_health_workout_raw_evidence.sql +95 -0
- package/skills/forge-openclaw/SKILL.md +25 -1
- package/skills/forge-openclaw/entity_conversation_playbooks.md +46 -14
- package/dist/assets/index-B9IVt8VN.js +0 -90
- package/dist/assets/index-DJlo9Tsp.css +0 -1
- package/dist/assets/vendor-BcOHGipZ.js +0 -1341
- package/dist/assets/vendor-DT3pnAKJ.css +0 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { scanConversations } from "forge-devrage";
|
|
3
|
+
import { getDatabase, runInTransaction } from "../db.js";
|
|
4
|
+
import { psycheMetricsViewDataSchema } from "../psyche-types.js";
|
|
5
|
+
const SWEAR_COUNT_KEY = "swear_count";
|
|
6
|
+
const SWEARING_MESSAGE_PERCENT_KEY = "swearing_message_percent";
|
|
7
|
+
const DEFAULT_ROLE_FILTER = new Set(["user"]);
|
|
8
|
+
const PSYCHE_METRIC_DEFINITIONS = {
|
|
9
|
+
[SWEAR_COUNT_KEY]: {
|
|
10
|
+
metric: "devrageSwearCount",
|
|
11
|
+
label: "Devrage swears",
|
|
12
|
+
category: "conversationTone",
|
|
13
|
+
unit: "swears",
|
|
14
|
+
aggregation: "cumulative"
|
|
15
|
+
},
|
|
16
|
+
[SWEARING_MESSAGE_PERCENT_KEY]: {
|
|
17
|
+
metric: "swearingMessagePercent",
|
|
18
|
+
label: "Swearing messages",
|
|
19
|
+
category: "conversationTone",
|
|
20
|
+
unit: "%",
|
|
21
|
+
aggregation: "discrete"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let syncInFlight = null;
|
|
25
|
+
export async function syncDevrageMetricHistory(options = {}) {
|
|
26
|
+
if (syncInFlight) {
|
|
27
|
+
return syncInFlight;
|
|
28
|
+
}
|
|
29
|
+
syncInFlight = syncDevrageMetricHistoryInternal(options).finally(() => {
|
|
30
|
+
syncInFlight = null;
|
|
31
|
+
});
|
|
32
|
+
return syncInFlight;
|
|
33
|
+
}
|
|
34
|
+
export async function syncDevrageMetricHistoryIfNeeded() {
|
|
35
|
+
const state = getDevrageSyncState();
|
|
36
|
+
if (!state?.full_sync_completed_at) {
|
|
37
|
+
await syncDevrageMetricHistory({ forceFull: true });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const today = todayDateKey();
|
|
41
|
+
if (state.last_synced_date_key !== today) {
|
|
42
|
+
await syncDevrageMetricHistory({ dateKey: today });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function getDevrageMetricPayload() {
|
|
46
|
+
const generatedAt = nowIso();
|
|
47
|
+
const state = getDevrageSyncState();
|
|
48
|
+
const history = getDevrageDailyHistory(90);
|
|
49
|
+
const latest = history[0] ?? null;
|
|
50
|
+
const dailyAverages = getMetricAverages();
|
|
51
|
+
const weeklyAverages = getMetricAverages(7);
|
|
52
|
+
return {
|
|
53
|
+
generatedAt,
|
|
54
|
+
hasData: history.some((day) => day.conversationsScanned > 0),
|
|
55
|
+
latestDateKey: latest?.dateKey ?? null,
|
|
56
|
+
rawSwearCount: latest?.rawSwearCount ?? 0,
|
|
57
|
+
swearingMessagePercent: latest?.swearingMessagePercent ?? 0,
|
|
58
|
+
conversationsScanned: latest?.conversationsScanned ?? 0,
|
|
59
|
+
messagesScanned: latest?.messagesScanned ?? 0,
|
|
60
|
+
messagesWithSwears: latest?.messagesWithSwears ?? 0,
|
|
61
|
+
dailyAverage: {
|
|
62
|
+
rawSwearCount: dailyAverages.rawSwearCount,
|
|
63
|
+
swearingMessagePercent: dailyAverages.swearingMessagePercent
|
|
64
|
+
},
|
|
65
|
+
weeklyAverage: {
|
|
66
|
+
rawSwearCount: weeklyAverages.rawSwearCount,
|
|
67
|
+
swearingMessagePercent: weeklyAverages.swearingMessagePercent
|
|
68
|
+
},
|
|
69
|
+
history,
|
|
70
|
+
sync: {
|
|
71
|
+
fullSyncCompletedAt: state?.full_sync_completed_at ?? null,
|
|
72
|
+
lastDailySyncAt: state?.last_daily_sync_at ?? null,
|
|
73
|
+
lastSyncedDateKey: state?.last_synced_date_key ?? null
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function getPsycheMetricsViewData() {
|
|
78
|
+
const rows = getDatabase()
|
|
79
|
+
.prepare(`SELECT date_key, metric_key, value, unit, sample_count, computed_at
|
|
80
|
+
FROM psyche_devrage_metric_measures
|
|
81
|
+
ORDER BY date_key ASC, metric_key ASC`)
|
|
82
|
+
.all();
|
|
83
|
+
const metricBuckets = new Map();
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const definition = PSYCHE_METRIC_DEFINITIONS[row.metric_key];
|
|
86
|
+
if (!definition) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const bucket = metricBuckets.get(definition.metric) ?? {
|
|
90
|
+
label: definition.label,
|
|
91
|
+
category: definition.category,
|
|
92
|
+
unit: definition.unit,
|
|
93
|
+
aggregation: definition.aggregation,
|
|
94
|
+
days: new Map()
|
|
95
|
+
};
|
|
96
|
+
const dayRows = bucket.days.get(row.date_key) ?? [];
|
|
97
|
+
dayRows.push(row);
|
|
98
|
+
bucket.days.set(row.date_key, dayRows);
|
|
99
|
+
metricBuckets.set(definition.metric, bucket);
|
|
100
|
+
}
|
|
101
|
+
const metrics = [...metricBuckets.entries()]
|
|
102
|
+
.map(([metric, bucket]) => {
|
|
103
|
+
const days = [...bucket.days.entries()]
|
|
104
|
+
.sort((left, right) => left[0].localeCompare(right[0]))
|
|
105
|
+
.map(([dateKey, entries]) => {
|
|
106
|
+
const values = entries.map((entry) => Number(entry.value) || 0);
|
|
107
|
+
const value = average(values);
|
|
108
|
+
return {
|
|
109
|
+
dateKey,
|
|
110
|
+
average: round(value, bucket.aggregation === "cumulative" ? 0 : 1),
|
|
111
|
+
minimum: round(Math.min(...values), bucket.aggregation === "cumulative" ? 0 : 1),
|
|
112
|
+
maximum: round(Math.max(...values), bucket.aggregation === "cumulative" ? 0 : 1),
|
|
113
|
+
latest: round(values.at(-1) ?? value, bucket.aggregation === "cumulative" ? 0 : 1),
|
|
114
|
+
total: bucket.aggregation === "cumulative"
|
|
115
|
+
? round(sumNullable(values), 0)
|
|
116
|
+
: null,
|
|
117
|
+
sampleCount: entries.reduce((sum, entry) => sum + Number(entry.sample_count || 0), 0),
|
|
118
|
+
latestSampleAt: entries
|
|
119
|
+
.map((entry) => entry.computed_at)
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.sort()
|
|
122
|
+
.at(-1) ?? null
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const latestDay = [...days].reverse().find((day) => psycheMetricPrimaryValue({
|
|
126
|
+
aggregation: bucket.aggregation,
|
|
127
|
+
latest: day.latest,
|
|
128
|
+
average: day.average,
|
|
129
|
+
total: day.total,
|
|
130
|
+
maximum: day.maximum
|
|
131
|
+
}) !== null) ?? null;
|
|
132
|
+
const recentValues = days
|
|
133
|
+
.map((day) => psycheMetricPrimaryValue({
|
|
134
|
+
aggregation: bucket.aggregation,
|
|
135
|
+
latest: day.latest,
|
|
136
|
+
average: day.average,
|
|
137
|
+
total: day.total,
|
|
138
|
+
maximum: day.maximum
|
|
139
|
+
}))
|
|
140
|
+
.filter((value) => value != null);
|
|
141
|
+
const baselineValues = recentValues.slice(Math.max(0, recentValues.length - 8), recentValues.length - 1);
|
|
142
|
+
const baselineValue = baselineValues.length > 0 ? average(baselineValues) : recentValues.at(-2) ?? null;
|
|
143
|
+
const latestValue = latestDay
|
|
144
|
+
? psycheMetricPrimaryValue({
|
|
145
|
+
aggregation: bucket.aggregation,
|
|
146
|
+
latest: latestDay.latest,
|
|
147
|
+
average: latestDay.average,
|
|
148
|
+
total: latestDay.total,
|
|
149
|
+
maximum: latestDay.maximum
|
|
150
|
+
})
|
|
151
|
+
: null;
|
|
152
|
+
const digits = bucket.aggregation === "cumulative" ? 0 : 1;
|
|
153
|
+
return {
|
|
154
|
+
metric,
|
|
155
|
+
label: bucket.label,
|
|
156
|
+
category: bucket.category,
|
|
157
|
+
unit: bucket.unit,
|
|
158
|
+
aggregation: bucket.aggregation,
|
|
159
|
+
latestValue: latestValue == null ? null : round(latestValue, digits),
|
|
160
|
+
latestDateKey: latestDay?.dateKey ?? null,
|
|
161
|
+
baselineValue: baselineValue == null ? null : round(baselineValue, digits),
|
|
162
|
+
deltaValue: latestValue != null && baselineValue != null
|
|
163
|
+
? round(latestValue - baselineValue, digits)
|
|
164
|
+
: null,
|
|
165
|
+
coverageDays: days.filter((day) => day.sampleCount > 0).length,
|
|
166
|
+
days
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
.sort((left, right) => {
|
|
170
|
+
if (left.category === right.category) {
|
|
171
|
+
return left.label.localeCompare(right.label);
|
|
172
|
+
}
|
|
173
|
+
return left.category.localeCompare(right.category);
|
|
174
|
+
});
|
|
175
|
+
const latestDateKey = rows.map((row) => row.date_key).sort().at(-1) ?? null;
|
|
176
|
+
const trackedDays = new Set(rows.map((row) => row.date_key)).size;
|
|
177
|
+
const categoryBreakdown = [...new Set(metrics.map((metric) => metric.category))]
|
|
178
|
+
.map((category) => {
|
|
179
|
+
const categoryMetrics = metrics.filter((metric) => metric.category === category);
|
|
180
|
+
return {
|
|
181
|
+
category,
|
|
182
|
+
metricCount: categoryMetrics.length,
|
|
183
|
+
coverageDays: Math.max(...categoryMetrics.map((metric) => metric.coverageDays), 0)
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
.sort((left, right) => right.metricCount - left.metricCount);
|
|
187
|
+
const context = getDevrageConversationTotals();
|
|
188
|
+
const dailyAverages = getMetricAverages();
|
|
189
|
+
const weeklyAverages = getMetricAverages(7);
|
|
190
|
+
const state = getDevrageSyncState();
|
|
191
|
+
return psycheMetricsViewDataSchema.parse({
|
|
192
|
+
summary: {
|
|
193
|
+
hasData: metrics.length > 0 && context.conversations > 0,
|
|
194
|
+
trackedDays,
|
|
195
|
+
metricCount: metrics.length,
|
|
196
|
+
latestDateKey,
|
|
197
|
+
latestMetricCount: metrics.filter((metric) => metric.latestDateKey === latestDateKey).length,
|
|
198
|
+
categoryBreakdown
|
|
199
|
+
},
|
|
200
|
+
context: {
|
|
201
|
+
generatedAt: nowIso(),
|
|
202
|
+
conversationsScanned: Number(context.conversations) || 0,
|
|
203
|
+
sourceCount: Number(context.sources) || 0,
|
|
204
|
+
messagesScanned: Number(context.messages) || 0,
|
|
205
|
+
messagesWithSwears: Number(context.messages_with_swears) || 0,
|
|
206
|
+
totalSwears: Number(context.swear_count) || 0,
|
|
207
|
+
dailyAverage: {
|
|
208
|
+
rawSwearCount: dailyAverages.rawSwearCount,
|
|
209
|
+
swearingMessagePercent: dailyAverages.swearingMessagePercent
|
|
210
|
+
},
|
|
211
|
+
weeklyAverage: {
|
|
212
|
+
rawSwearCount: weeklyAverages.rawSwearCount,
|
|
213
|
+
swearingMessagePercent: weeklyAverages.swearingMessagePercent
|
|
214
|
+
},
|
|
215
|
+
sync: {
|
|
216
|
+
fullSyncCompletedAt: state?.full_sync_completed_at ?? null,
|
|
217
|
+
lastDailySyncAt: state?.last_daily_sync_at ?? null,
|
|
218
|
+
lastSyncedDateKey: state?.last_synced_date_key ?? null
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
metrics
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function syncDevrageMetricHistoryInternal(options) {
|
|
225
|
+
const dateKey = options.forceFull ? undefined : options.dateKey ?? todayDateKey();
|
|
226
|
+
const report = await scanConversations({
|
|
227
|
+
roles: DEFAULT_ROLE_FILTER,
|
|
228
|
+
date: dateKey,
|
|
229
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
230
|
+
});
|
|
231
|
+
storeDevrageReport(report, {
|
|
232
|
+
fullSync: Boolean(options.forceFull),
|
|
233
|
+
syncedDateKey: dateKey ?? null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
export function storeDevrageReport(report, options) {
|
|
237
|
+
const scannedAt = nowIso();
|
|
238
|
+
const affectedDateKeys = new Set(report.conversations.map((conversation) => conversation.dateKey));
|
|
239
|
+
runInTransaction(() => {
|
|
240
|
+
const database = getDatabase();
|
|
241
|
+
const deleteDate = database.prepare(`DELETE FROM psyche_devrage_conversation_measures WHERE date_key = ?`);
|
|
242
|
+
const insertConversation = database.prepare(`INSERT INTO psyche_devrage_conversation_measures (
|
|
243
|
+
id, source, conversation_id, date_key, updated_at, messages,
|
|
244
|
+
messages_with_swears, swear_count, scanned_at
|
|
245
|
+
)
|
|
246
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
247
|
+
ON CONFLICT(source, conversation_id, date_key) DO UPDATE SET
|
|
248
|
+
updated_at = excluded.updated_at,
|
|
249
|
+
messages = excluded.messages,
|
|
250
|
+
messages_with_swears = excluded.messages_with_swears,
|
|
251
|
+
swear_count = excluded.swear_count,
|
|
252
|
+
scanned_at = excluded.scanned_at`);
|
|
253
|
+
for (const dateKey of affectedDateKeys) {
|
|
254
|
+
deleteDate.run(dateKey);
|
|
255
|
+
}
|
|
256
|
+
for (const conversation of report.conversations) {
|
|
257
|
+
insertConversation.run(stableId("devrage_conversation", conversation.source, conversation.conversationId, conversation.dateKey), conversation.source, conversation.conversationId, conversation.dateKey, conversation.updatedAt, conversation.messages, conversation.messagesWithSwears, conversation.swears, scannedAt);
|
|
258
|
+
}
|
|
259
|
+
for (const dateKey of affectedDateKeys) {
|
|
260
|
+
recomputeMetricMeasuresForDate(dateKey, scannedAt);
|
|
261
|
+
}
|
|
262
|
+
upsertSyncState({
|
|
263
|
+
fullSyncCompletedAt: options.fullSync ? scannedAt : undefined,
|
|
264
|
+
lastDailySyncAt: options.fullSync ? undefined : scannedAt,
|
|
265
|
+
lastSyncedDateKey: options.syncedDateKey ?? [...affectedDateKeys].sort().at(-1) ?? null,
|
|
266
|
+
updatedAt: scannedAt
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function recomputeMetricMeasuresForDate(dateKey, computedAt) {
|
|
271
|
+
const aggregate = getDatabase()
|
|
272
|
+
.prepare(`SELECT
|
|
273
|
+
COUNT(*) AS conversations,
|
|
274
|
+
COALESCE(SUM(messages), 0) AS messages,
|
|
275
|
+
COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
|
|
276
|
+
COALESCE(SUM(swear_count), 0) AS swear_count
|
|
277
|
+
FROM psyche_devrage_conversation_measures
|
|
278
|
+
WHERE date_key = ?`)
|
|
279
|
+
.get(dateKey);
|
|
280
|
+
const messages = Number(aggregate.messages) || 0;
|
|
281
|
+
const messagesWithSwears = Number(aggregate.messages_with_swears) || 0;
|
|
282
|
+
const swearCount = Number(aggregate.swear_count) || 0;
|
|
283
|
+
const percent = messages > 0 ? (messagesWithSwears / messages) * 100 : 0;
|
|
284
|
+
upsertMetricMeasure(dateKey, SWEAR_COUNT_KEY, swearCount, "count", Number(aggregate.conversations) || 0, computedAt);
|
|
285
|
+
upsertMetricMeasure(dateKey, SWEARING_MESSAGE_PERCENT_KEY, percent, "percent", messages, computedAt);
|
|
286
|
+
}
|
|
287
|
+
function upsertMetricMeasure(dateKey, metricKey, value, unit, sampleCount, computedAt) {
|
|
288
|
+
getDatabase()
|
|
289
|
+
.prepare(`INSERT INTO psyche_devrage_metric_measures (
|
|
290
|
+
id, date_key, metric_key, value, unit, sample_count, computed_at
|
|
291
|
+
)
|
|
292
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
293
|
+
ON CONFLICT(date_key, metric_key) DO UPDATE SET
|
|
294
|
+
value = excluded.value,
|
|
295
|
+
unit = excluded.unit,
|
|
296
|
+
sample_count = excluded.sample_count,
|
|
297
|
+
computed_at = excluded.computed_at`)
|
|
298
|
+
.run(stableId("devrage_metric", dateKey, metricKey), dateKey, metricKey, value, unit, sampleCount, computedAt);
|
|
299
|
+
}
|
|
300
|
+
function upsertSyncState(input) {
|
|
301
|
+
const current = getDevrageSyncState();
|
|
302
|
+
getDatabase()
|
|
303
|
+
.prepare(`INSERT INTO psyche_devrage_sync_state (
|
|
304
|
+
id, full_sync_completed_at, last_daily_sync_at, last_synced_date_key, updated_at
|
|
305
|
+
)
|
|
306
|
+
VALUES ('default', ?, ?, ?, ?)
|
|
307
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
308
|
+
full_sync_completed_at = excluded.full_sync_completed_at,
|
|
309
|
+
last_daily_sync_at = excluded.last_daily_sync_at,
|
|
310
|
+
last_synced_date_key = excluded.last_synced_date_key,
|
|
311
|
+
updated_at = excluded.updated_at`)
|
|
312
|
+
.run(input.fullSyncCompletedAt ?? current?.full_sync_completed_at ?? null, input.lastDailySyncAt ?? current?.last_daily_sync_at ?? null, input.lastSyncedDateKey ?? current?.last_synced_date_key ?? null, input.updatedAt);
|
|
313
|
+
}
|
|
314
|
+
function getDevrageSyncState() {
|
|
315
|
+
return (getDatabase()
|
|
316
|
+
.prepare(`SELECT full_sync_completed_at, last_daily_sync_at, last_synced_date_key, updated_at
|
|
317
|
+
FROM psyche_devrage_sync_state
|
|
318
|
+
WHERE id = 'default'`)
|
|
319
|
+
.get() ?? null);
|
|
320
|
+
}
|
|
321
|
+
function getDevrageDailyHistory(limit) {
|
|
322
|
+
const rows = getDatabase()
|
|
323
|
+
.prepare(`SELECT
|
|
324
|
+
date_key,
|
|
325
|
+
COUNT(*) AS conversations,
|
|
326
|
+
COALESCE(SUM(messages), 0) AS messages,
|
|
327
|
+
COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
|
|
328
|
+
COALESCE(SUM(swear_count), 0) AS swear_count
|
|
329
|
+
FROM psyche_devrage_conversation_measures
|
|
330
|
+
GROUP BY date_key
|
|
331
|
+
ORDER BY date_key DESC
|
|
332
|
+
LIMIT ?`)
|
|
333
|
+
.all(limit);
|
|
334
|
+
return rows.map((row) => {
|
|
335
|
+
const messages = Number(row.messages) || 0;
|
|
336
|
+
const messagesWithSwears = Number(row.messages_with_swears) || 0;
|
|
337
|
+
return {
|
|
338
|
+
dateKey: row.date_key,
|
|
339
|
+
rawSwearCount: Number(row.swear_count) || 0,
|
|
340
|
+
swearingMessagePercent: messages > 0 ? (messagesWithSwears / messages) * 100 : 0,
|
|
341
|
+
conversationsScanned: Number(row.conversations) || 0,
|
|
342
|
+
messagesScanned: messages,
|
|
343
|
+
messagesWithSwears
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function getMetricAverages(days) {
|
|
348
|
+
const where = days
|
|
349
|
+
? `WHERE date_key IN (
|
|
350
|
+
SELECT date_key FROM psyche_devrage_metric_measures
|
|
351
|
+
GROUP BY date_key
|
|
352
|
+
ORDER BY date_key DESC
|
|
353
|
+
LIMIT ?
|
|
354
|
+
)`
|
|
355
|
+
: "";
|
|
356
|
+
const rows = getDatabase()
|
|
357
|
+
.prepare(`SELECT metric_key, AVG(value) AS value
|
|
358
|
+
FROM psyche_devrage_metric_measures
|
|
359
|
+
${where}
|
|
360
|
+
GROUP BY metric_key`)
|
|
361
|
+
.all(...(days ? [days] : []));
|
|
362
|
+
const swearAverage = rows.find((row) => row.metric_key === SWEAR_COUNT_KEY)?.value ?? 0;
|
|
363
|
+
const percentAverage = rows.find((row) => row.metric_key === SWEARING_MESSAGE_PERCENT_KEY)?.value ?? 0;
|
|
364
|
+
return {
|
|
365
|
+
rawSwearCount: round(Number(swearAverage) || 0, 1),
|
|
366
|
+
swearingMessagePercent: round(Number(percentAverage) || 0, 1)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function getDevrageConversationTotals() {
|
|
370
|
+
return getDatabase()
|
|
371
|
+
.prepare(`SELECT
|
|
372
|
+
COUNT(*) AS conversations,
|
|
373
|
+
COUNT(DISTINCT source) AS sources,
|
|
374
|
+
COALESCE(SUM(messages), 0) AS messages,
|
|
375
|
+
COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
|
|
376
|
+
COALESCE(SUM(swear_count), 0) AS swear_count
|
|
377
|
+
FROM psyche_devrage_conversation_measures`)
|
|
378
|
+
.get();
|
|
379
|
+
}
|
|
380
|
+
function psycheMetricPrimaryValue(metric) {
|
|
381
|
+
if (metric.aggregation === "cumulative") {
|
|
382
|
+
return metric.total ?? metric.latest;
|
|
383
|
+
}
|
|
384
|
+
return metric.latest ?? metric.average ?? metric.maximum;
|
|
385
|
+
}
|
|
386
|
+
function average(values) {
|
|
387
|
+
if (values.length === 0) {
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
391
|
+
}
|
|
392
|
+
function sumNullable(values) {
|
|
393
|
+
return values.reduce((sum, value) => sum + value, 0);
|
|
394
|
+
}
|
|
395
|
+
function stableId(prefix, ...parts) {
|
|
396
|
+
const digest = createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 20);
|
|
397
|
+
return `${prefix}_${digest}`;
|
|
398
|
+
}
|
|
399
|
+
function todayDateKey() {
|
|
400
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
401
|
+
year: "numeric",
|
|
402
|
+
month: "2-digit",
|
|
403
|
+
day: "2-digit"
|
|
404
|
+
}).format(new Date());
|
|
405
|
+
}
|
|
406
|
+
function nowIso() {
|
|
407
|
+
return new Date().toISOString();
|
|
408
|
+
}
|
|
409
|
+
function round(value, digits) {
|
|
410
|
+
const factor = 10 ** digits;
|
|
411
|
+
return Math.round(value * factor) / factor;
|
|
412
|
+
}
|
|
@@ -3,6 +3,7 @@ import { listInsights } from "../repositories/collaboration.js";
|
|
|
3
3
|
import { listNotes } from "../repositories/notes.js";
|
|
4
4
|
import { filterOwnedEntities } from "../repositories/entity-ownership.js";
|
|
5
5
|
import { listBehaviorPatterns, listBehaviors, listBeliefEntries, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports } from "../repositories/psyche.js";
|
|
6
|
+
import { getDevrageMetricPayload } from "./devrage.js";
|
|
6
7
|
import { psycheOverviewPayloadSchema } from "../psyche-types.js";
|
|
7
8
|
const PSYCHE_ENTITY_TYPE_SET = new Set([
|
|
8
9
|
"psyche_value",
|
|
@@ -32,6 +33,7 @@ export function getPsycheOverview(userIds) {
|
|
|
32
33
|
...behaviors.filter((behavior) => behavior.kind === "committed").map((behavior) => behavior.title),
|
|
33
34
|
...reports.flatMap((report) => report.nextMoves)
|
|
34
35
|
];
|
|
36
|
+
const devrageMetric = getDevrageMetricPayload();
|
|
35
37
|
const schemaPressure = schemaCatalog
|
|
36
38
|
.filter((schema) => schema.schemaType === "maladaptive")
|
|
37
39
|
.map((schema) => {
|
|
@@ -57,6 +59,7 @@ export function getPsycheOverview(userIds) {
|
|
|
57
59
|
modes,
|
|
58
60
|
reports,
|
|
59
61
|
schemaPressure,
|
|
62
|
+
devrageMetric,
|
|
60
63
|
openInsights,
|
|
61
64
|
openNotes,
|
|
62
65
|
committedActions
|
|
@@ -500,6 +500,9 @@ export function getPsycheOverview(userIds) {
|
|
|
500
500
|
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
|
501
501
|
return request(`/api/v1/psyche/overview${suffix}`);
|
|
502
502
|
}
|
|
503
|
+
export function getPsycheMetricsView() {
|
|
504
|
+
return request("/api/v1/psyche/metrics");
|
|
505
|
+
}
|
|
503
506
|
export function listQuestionnaires(userIds) {
|
|
504
507
|
const search = new URLSearchParams();
|
|
505
508
|
appendUserIds(search, coerceUserIds(userIds));
|
|
@@ -1688,6 +1691,16 @@ export function getFitnessView(userIds) {
|
|
|
1688
1691
|
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
|
1689
1692
|
return request(`/api/v1/health/fitness${suffix}`);
|
|
1690
1693
|
}
|
|
1694
|
+
export function getWorkoutDetail(workoutId, resolution = "adaptive") {
|
|
1695
|
+
const search = new URLSearchParams({ resolution });
|
|
1696
|
+
return request(`/api/v1/health/workouts/${workoutId}/detail?${search.toString()}`);
|
|
1697
|
+
}
|
|
1698
|
+
export function getHealthZoneProfile(userIds) {
|
|
1699
|
+
const search = new URLSearchParams();
|
|
1700
|
+
appendUserIds(search, coerceUserIds(userIds));
|
|
1701
|
+
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
|
1702
|
+
return request(`/api/v1/health/zone-profile${suffix}`);
|
|
1703
|
+
}
|
|
1691
1704
|
export function getVitalsView(userIds) {
|
|
1692
1705
|
const search = new URLSearchParams();
|
|
1693
1706
|
appendUserIds(search, coerceUserIds(userIds));
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.2.69",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS psyche_devrage_conversation_measures (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
source TEXT NOT NULL,
|
|
4
|
+
conversation_id TEXT NOT NULL,
|
|
5
|
+
date_key TEXT NOT NULL,
|
|
6
|
+
updated_at TEXT NOT NULL,
|
|
7
|
+
messages INTEGER NOT NULL DEFAULT 0,
|
|
8
|
+
messages_with_swears INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
swear_count INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
scanned_at TEXT NOT NULL,
|
|
11
|
+
UNIQUE (source, conversation_id, date_key)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS psyche_devrage_conversation_measures_date_idx
|
|
15
|
+
ON psyche_devrage_conversation_measures (date_key DESC);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS psyche_devrage_conversation_measures_updated_idx
|
|
18
|
+
ON psyche_devrage_conversation_measures (updated_at DESC);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS psyche_devrage_metric_measures (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
date_key TEXT NOT NULL,
|
|
23
|
+
metric_key TEXT NOT NULL,
|
|
24
|
+
value REAL NOT NULL,
|
|
25
|
+
unit TEXT NOT NULL DEFAULT '',
|
|
26
|
+
sample_count INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
computed_at TEXT NOT NULL,
|
|
28
|
+
UNIQUE (date_key, metric_key)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS psyche_devrage_metric_measures_metric_date_idx
|
|
32
|
+
ON psyche_devrage_metric_measures (metric_key, date_key DESC);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS psyche_devrage_sync_state (
|
|
35
|
+
id TEXT PRIMARY KEY CHECK (id = 'default'),
|
|
36
|
+
full_sync_completed_at TEXT,
|
|
37
|
+
last_daily_sync_at TEXT,
|
|
38
|
+
last_synced_date_key TEXT,
|
|
39
|
+
updated_at TEXT NOT NULL
|
|
40
|
+
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS health_workout_time_series (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
workout_id TEXT NOT NULL REFERENCES health_workout_sessions(id) ON DELETE CASCADE,
|
|
4
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
5
|
+
source_sample_uid TEXT NOT NULL,
|
|
6
|
+
series_index INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
metric_key TEXT NOT NULL,
|
|
8
|
+
label TEXT NOT NULL DEFAULT '',
|
|
9
|
+
category TEXT NOT NULL DEFAULT '',
|
|
10
|
+
unit TEXT NOT NULL DEFAULT '',
|
|
11
|
+
value REAL NOT NULL,
|
|
12
|
+
started_at TEXT NOT NULL,
|
|
13
|
+
ended_at TEXT NOT NULL,
|
|
14
|
+
source_device TEXT NOT NULL DEFAULT '',
|
|
15
|
+
source_bundle_identifier TEXT,
|
|
16
|
+
source_product_type TEXT,
|
|
17
|
+
capture_method TEXT NOT NULL DEFAULT 'associated_workout',
|
|
18
|
+
quality_flags_json TEXT NOT NULL DEFAULT '[]',
|
|
19
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
20
|
+
provenance_json TEXT NOT NULL DEFAULT '{}',
|
|
21
|
+
created_at TEXT NOT NULL,
|
|
22
|
+
updated_at TEXT NOT NULL,
|
|
23
|
+
UNIQUE (workout_id, metric_key, source_sample_uid, series_index)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_health_workout_time_series_workout_metric
|
|
27
|
+
ON health_workout_time_series(workout_id, metric_key, started_at);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_health_workout_time_series_user_metric
|
|
30
|
+
ON health_workout_time_series(user_id, metric_key, started_at DESC);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS health_workout_routes (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
workout_id TEXT NOT NULL REFERENCES health_workout_sessions(id) ON DELETE CASCADE,
|
|
35
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
36
|
+
source_route_uid TEXT NOT NULL,
|
|
37
|
+
point_index INTEGER NOT NULL,
|
|
38
|
+
recorded_at TEXT NOT NULL,
|
|
39
|
+
latitude REAL NOT NULL,
|
|
40
|
+
longitude REAL NOT NULL,
|
|
41
|
+
altitude_meters REAL,
|
|
42
|
+
horizontal_accuracy_meters REAL,
|
|
43
|
+
vertical_accuracy_meters REAL,
|
|
44
|
+
speed_mps REAL,
|
|
45
|
+
course_degrees REAL,
|
|
46
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
47
|
+
provenance_json TEXT NOT NULL DEFAULT '{}',
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
updated_at TEXT NOT NULL,
|
|
50
|
+
UNIQUE (workout_id, source_route_uid, point_index)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_health_workout_routes_workout
|
|
54
|
+
ON health_workout_routes(workout_id, point_index);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS health_zone_profiles (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
59
|
+
model_version TEXT NOT NULL DEFAULT 'forge-hrr-v1',
|
|
60
|
+
birth_year INTEGER,
|
|
61
|
+
sex_at_birth TEXT,
|
|
62
|
+
known_max_hr REAL,
|
|
63
|
+
threshold_hr REAL,
|
|
64
|
+
resting_hr_override REAL,
|
|
65
|
+
custom_zones_json TEXT NOT NULL DEFAULT '[]',
|
|
66
|
+
inferred_max_hr REAL,
|
|
67
|
+
inferred_resting_hr REAL,
|
|
68
|
+
confidence TEXT NOT NULL DEFAULT 'medium',
|
|
69
|
+
thresholds_json TEXT NOT NULL DEFAULT '[]',
|
|
70
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
updated_at TEXT NOT NULL,
|
|
73
|
+
UNIQUE(user_id, model_version)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS health_workout_analytics (
|
|
77
|
+
id TEXT PRIMARY KEY,
|
|
78
|
+
workout_id TEXT NOT NULL REFERENCES health_workout_sessions(id) ON DELETE CASCADE,
|
|
79
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
80
|
+
zone_profile_id TEXT REFERENCES health_zone_profiles(id) ON DELETE SET NULL,
|
|
81
|
+
model_version TEXT NOT NULL DEFAULT 'forge-hrr-v1',
|
|
82
|
+
confidence TEXT NOT NULL DEFAULT 'unavailable',
|
|
83
|
+
data_quality_json TEXT NOT NULL DEFAULT '{}',
|
|
84
|
+
zone_durations_json TEXT NOT NULL DEFAULT '[]',
|
|
85
|
+
hr_summary_json TEXT NOT NULL DEFAULT '{}',
|
|
86
|
+
load_json TEXT NOT NULL DEFAULT '{}',
|
|
87
|
+
route_summary_json TEXT NOT NULL DEFAULT '{}',
|
|
88
|
+
computed_at TEXT NOT NULL,
|
|
89
|
+
created_at TEXT NOT NULL,
|
|
90
|
+
updated_at TEXT NOT NULL,
|
|
91
|
+
UNIQUE(workout_id, model_version)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_health_workout_analytics_user
|
|
95
|
+
ON health_workout_analytics(user_id, computed_at DESC);
|