forge-openclaw-plugin 0.2.68 → 0.2.70
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/061_health_workout_raw_evidence.sql +95 -0
- package/dist/server/server/migrations/062_health_mobile_sync_sessions.sql +55 -0
- package/dist/server/server/src/app.js +149 -21
- package/dist/server/server/src/health-workout-analytics.js +572 -0
- package/dist/server/server/src/health.js +612 -4
- package/dist/server/server/src/openapi.js +162 -0
- package/dist/server/server/src/psyche-types.js +59 -0
- package/dist/server/server/src/services/devrage.js +191 -0
- package/dist/server/src/lib/api.js +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/server/migrations/061_health_workout_raw_evidence.sql +95 -0
- package/server/migrations/062_health_mobile_sync_sessions.sql +55 -0
- package/skills/forge-openclaw/SKILL.md +35 -6
- package/skills/forge-openclaw/entity_conversation_playbooks.md +179 -18
- package/dist/assets/index-B0PIKEnz.css +0 -1
- package/dist/assets/index-BofyMuFh.js +0 -90
- package/dist/assets/vendor-BcOHGipZ.js +0 -1341
- package/dist/assets/vendor-DT3pnAKJ.css +0 -1
|
@@ -4650,6 +4650,7 @@ export function buildOpenApiDocument() {
|
|
|
4650
4650
|
additionalProperties: false,
|
|
4651
4651
|
required: [
|
|
4652
4652
|
"generatedAt",
|
|
4653
|
+
"hasData",
|
|
4653
4654
|
"latestDateKey",
|
|
4654
4655
|
"rawSwearCount",
|
|
4655
4656
|
"swearingMessagePercent",
|
|
@@ -4663,6 +4664,7 @@ export function buildOpenApiDocument() {
|
|
|
4663
4664
|
],
|
|
4664
4665
|
properties: {
|
|
4665
4666
|
generatedAt: { type: "string", format: "date-time" },
|
|
4667
|
+
hasData: { type: "boolean" },
|
|
4666
4668
|
latestDateKey: nullable({ type: "string" }),
|
|
4667
4669
|
rawSwearCount: { type: "number" },
|
|
4668
4670
|
swearingMessagePercent: { type: "number" },
|
|
@@ -4719,6 +4721,148 @@ export function buildOpenApiDocument() {
|
|
|
4719
4721
|
}
|
|
4720
4722
|
}
|
|
4721
4723
|
};
|
|
4724
|
+
const dailyMetricDayRecord = {
|
|
4725
|
+
type: "object",
|
|
4726
|
+
additionalProperties: false,
|
|
4727
|
+
required: [
|
|
4728
|
+
"dateKey",
|
|
4729
|
+
"average",
|
|
4730
|
+
"minimum",
|
|
4731
|
+
"maximum",
|
|
4732
|
+
"latest",
|
|
4733
|
+
"total",
|
|
4734
|
+
"sampleCount",
|
|
4735
|
+
"latestSampleAt"
|
|
4736
|
+
],
|
|
4737
|
+
properties: {
|
|
4738
|
+
dateKey: { type: "string" },
|
|
4739
|
+
average: nullable({ type: "number" }),
|
|
4740
|
+
minimum: nullable({ type: "number" }),
|
|
4741
|
+
maximum: nullable({ type: "number" }),
|
|
4742
|
+
latest: nullable({ type: "number" }),
|
|
4743
|
+
total: nullable({ type: "number" }),
|
|
4744
|
+
sampleCount: { type: "integer" },
|
|
4745
|
+
latestSampleAt: nullable({ type: "string", format: "date-time" })
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
const dailyMetricRecord = {
|
|
4749
|
+
type: "object",
|
|
4750
|
+
additionalProperties: false,
|
|
4751
|
+
required: [
|
|
4752
|
+
"metric",
|
|
4753
|
+
"label",
|
|
4754
|
+
"category",
|
|
4755
|
+
"unit",
|
|
4756
|
+
"aggregation",
|
|
4757
|
+
"latestValue",
|
|
4758
|
+
"latestDateKey",
|
|
4759
|
+
"baselineValue",
|
|
4760
|
+
"deltaValue",
|
|
4761
|
+
"coverageDays",
|
|
4762
|
+
"days"
|
|
4763
|
+
],
|
|
4764
|
+
properties: {
|
|
4765
|
+
metric: { type: "string" },
|
|
4766
|
+
label: { type: "string" },
|
|
4767
|
+
category: { type: "string" },
|
|
4768
|
+
unit: { type: "string" },
|
|
4769
|
+
aggregation: { type: "string", enum: ["discrete", "cumulative"] },
|
|
4770
|
+
latestValue: nullable({ type: "number" }),
|
|
4771
|
+
latestDateKey: nullable({ type: "string" }),
|
|
4772
|
+
baselineValue: nullable({ type: "number" }),
|
|
4773
|
+
deltaValue: nullable({ type: "number" }),
|
|
4774
|
+
coverageDays: { type: "integer" },
|
|
4775
|
+
days: arrayOf(dailyMetricDayRecord)
|
|
4776
|
+
}
|
|
4777
|
+
};
|
|
4778
|
+
const psycheMetricsViewData = {
|
|
4779
|
+
type: "object",
|
|
4780
|
+
additionalProperties: false,
|
|
4781
|
+
required: ["summary", "context", "metrics"],
|
|
4782
|
+
properties: {
|
|
4783
|
+
summary: {
|
|
4784
|
+
type: "object",
|
|
4785
|
+
additionalProperties: false,
|
|
4786
|
+
required: [
|
|
4787
|
+
"hasData",
|
|
4788
|
+
"trackedDays",
|
|
4789
|
+
"metricCount",
|
|
4790
|
+
"latestDateKey",
|
|
4791
|
+
"latestMetricCount",
|
|
4792
|
+
"categoryBreakdown"
|
|
4793
|
+
],
|
|
4794
|
+
properties: {
|
|
4795
|
+
hasData: { type: "boolean" },
|
|
4796
|
+
trackedDays: { type: "integer" },
|
|
4797
|
+
metricCount: { type: "integer" },
|
|
4798
|
+
latestDateKey: nullable({ type: "string" }),
|
|
4799
|
+
latestMetricCount: { type: "integer" },
|
|
4800
|
+
categoryBreakdown: arrayOf({
|
|
4801
|
+
type: "object",
|
|
4802
|
+
additionalProperties: false,
|
|
4803
|
+
required: ["category", "metricCount", "coverageDays"],
|
|
4804
|
+
properties: {
|
|
4805
|
+
category: { type: "string" },
|
|
4806
|
+
metricCount: { type: "integer" },
|
|
4807
|
+
coverageDays: { type: "integer" }
|
|
4808
|
+
}
|
|
4809
|
+
})
|
|
4810
|
+
}
|
|
4811
|
+
},
|
|
4812
|
+
context: {
|
|
4813
|
+
type: "object",
|
|
4814
|
+
additionalProperties: false,
|
|
4815
|
+
required: [
|
|
4816
|
+
"generatedAt",
|
|
4817
|
+
"conversationsScanned",
|
|
4818
|
+
"sourceCount",
|
|
4819
|
+
"messagesScanned",
|
|
4820
|
+
"messagesWithSwears",
|
|
4821
|
+
"totalSwears",
|
|
4822
|
+
"dailyAverage",
|
|
4823
|
+
"weeklyAverage",
|
|
4824
|
+
"sync"
|
|
4825
|
+
],
|
|
4826
|
+
properties: {
|
|
4827
|
+
generatedAt: { type: "string", format: "date-time" },
|
|
4828
|
+
conversationsScanned: { type: "integer" },
|
|
4829
|
+
sourceCount: { type: "integer" },
|
|
4830
|
+
messagesScanned: { type: "integer" },
|
|
4831
|
+
messagesWithSwears: { type: "integer" },
|
|
4832
|
+
totalSwears: { type: "number" },
|
|
4833
|
+
dailyAverage: {
|
|
4834
|
+
type: "object",
|
|
4835
|
+
additionalProperties: false,
|
|
4836
|
+
required: ["rawSwearCount", "swearingMessagePercent"],
|
|
4837
|
+
properties: {
|
|
4838
|
+
rawSwearCount: { type: "number" },
|
|
4839
|
+
swearingMessagePercent: { type: "number" }
|
|
4840
|
+
}
|
|
4841
|
+
},
|
|
4842
|
+
weeklyAverage: {
|
|
4843
|
+
type: "object",
|
|
4844
|
+
additionalProperties: false,
|
|
4845
|
+
required: ["rawSwearCount", "swearingMessagePercent"],
|
|
4846
|
+
properties: {
|
|
4847
|
+
rawSwearCount: { type: "number" },
|
|
4848
|
+
swearingMessagePercent: { type: "number" }
|
|
4849
|
+
}
|
|
4850
|
+
},
|
|
4851
|
+
sync: {
|
|
4852
|
+
type: "object",
|
|
4853
|
+
additionalProperties: false,
|
|
4854
|
+
required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
|
|
4855
|
+
properties: {
|
|
4856
|
+
fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
|
|
4857
|
+
lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
|
|
4858
|
+
lastSyncedDateKey: nullable({ type: "string" })
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
},
|
|
4863
|
+
metrics: arrayOf(dailyMetricRecord)
|
|
4864
|
+
}
|
|
4865
|
+
};
|
|
4722
4866
|
const psycheOverviewPayload = {
|
|
4723
4867
|
type: "object",
|
|
4724
4868
|
additionalProperties: false,
|
|
@@ -5068,6 +5212,7 @@ export function buildOpenApiDocument() {
|
|
|
5068
5212
|
WorkoutSession: workoutSession,
|
|
5069
5213
|
SleepViewData: sleepViewData,
|
|
5070
5214
|
FitnessViewData: fitnessViewData,
|
|
5215
|
+
PsycheMetricsViewData: psycheMetricsViewData,
|
|
5071
5216
|
PsycheOverviewPayload: psycheOverviewPayload,
|
|
5072
5217
|
Insight: insight,
|
|
5073
5218
|
InsightFeedback: insightFeedback,
|
|
@@ -7120,6 +7265,23 @@ export function buildOpenApiDocument() {
|
|
|
7120
7265
|
}
|
|
7121
7266
|
}
|
|
7122
7267
|
},
|
|
7268
|
+
"/api/v1/psyche/metrics": {
|
|
7269
|
+
get: {
|
|
7270
|
+
summary: "Get daily Psyche metric history",
|
|
7271
|
+
responses: {
|
|
7272
|
+
"200": jsonResponse({
|
|
7273
|
+
type: "object",
|
|
7274
|
+
required: ["metrics"],
|
|
7275
|
+
properties: {
|
|
7276
|
+
metrics: {
|
|
7277
|
+
$ref: "#/components/schemas/PsycheMetricsViewData"
|
|
7278
|
+
}
|
|
7279
|
+
}
|
|
7280
|
+
}, "Psyche metrics view"),
|
|
7281
|
+
default: { $ref: "#/components/responses/Error" }
|
|
7282
|
+
}
|
|
7283
|
+
}
|
|
7284
|
+
},
|
|
7123
7285
|
"/api/v1/psyche/values": {
|
|
7124
7286
|
get: {
|
|
7125
7287
|
summary: "List ACT-style values",
|
|
@@ -246,6 +246,7 @@ export const schemaPressureEntrySchema = z.object({
|
|
|
246
246
|
});
|
|
247
247
|
export const devrageMetricPayloadSchema = z.object({
|
|
248
248
|
generatedAt: z.string(),
|
|
249
|
+
hasData: z.boolean(),
|
|
249
250
|
latestDateKey: z.string().nullable(),
|
|
250
251
|
rawSwearCount: z.number().nonnegative(),
|
|
251
252
|
swearingMessagePercent: z.number().nonnegative(),
|
|
@@ -274,6 +275,64 @@ export const devrageMetricPayloadSchema = z.object({
|
|
|
274
275
|
lastSyncedDateKey: z.string().nullable()
|
|
275
276
|
})
|
|
276
277
|
});
|
|
278
|
+
export const psycheMetricDayRecordSchema = z.object({
|
|
279
|
+
dateKey: z.string(),
|
|
280
|
+
average: z.number().nullable(),
|
|
281
|
+
minimum: z.number().nullable(),
|
|
282
|
+
maximum: z.number().nullable(),
|
|
283
|
+
latest: z.number().nullable(),
|
|
284
|
+
total: z.number().nullable(),
|
|
285
|
+
sampleCount: z.number().int().nonnegative(),
|
|
286
|
+
latestSampleAt: z.string().nullable()
|
|
287
|
+
});
|
|
288
|
+
export const psycheMetricsViewDataSchema = z.object({
|
|
289
|
+
summary: z.object({
|
|
290
|
+
hasData: z.boolean(),
|
|
291
|
+
trackedDays: z.number().int().nonnegative(),
|
|
292
|
+
metricCount: z.number().int().nonnegative(),
|
|
293
|
+
latestDateKey: z.string().nullable(),
|
|
294
|
+
latestMetricCount: z.number().int().nonnegative(),
|
|
295
|
+
categoryBreakdown: z.array(z.object({
|
|
296
|
+
category: z.string(),
|
|
297
|
+
metricCount: z.number().int().nonnegative(),
|
|
298
|
+
coverageDays: z.number().int().nonnegative()
|
|
299
|
+
}))
|
|
300
|
+
}),
|
|
301
|
+
context: z.object({
|
|
302
|
+
generatedAt: z.string(),
|
|
303
|
+
conversationsScanned: z.number().int().nonnegative(),
|
|
304
|
+
sourceCount: z.number().int().nonnegative(),
|
|
305
|
+
messagesScanned: z.number().int().nonnegative(),
|
|
306
|
+
messagesWithSwears: z.number().int().nonnegative(),
|
|
307
|
+
totalSwears: z.number().nonnegative(),
|
|
308
|
+
dailyAverage: z.object({
|
|
309
|
+
rawSwearCount: z.number().nonnegative(),
|
|
310
|
+
swearingMessagePercent: z.number().nonnegative()
|
|
311
|
+
}),
|
|
312
|
+
weeklyAverage: z.object({
|
|
313
|
+
rawSwearCount: z.number().nonnegative(),
|
|
314
|
+
swearingMessagePercent: z.number().nonnegative()
|
|
315
|
+
}),
|
|
316
|
+
sync: z.object({
|
|
317
|
+
fullSyncCompletedAt: z.string().nullable(),
|
|
318
|
+
lastDailySyncAt: z.string().nullable(),
|
|
319
|
+
lastSyncedDateKey: z.string().nullable()
|
|
320
|
+
})
|
|
321
|
+
}),
|
|
322
|
+
metrics: z.array(z.object({
|
|
323
|
+
metric: z.string(),
|
|
324
|
+
label: z.string(),
|
|
325
|
+
category: z.string(),
|
|
326
|
+
unit: z.string(),
|
|
327
|
+
aggregation: z.enum(["discrete", "cumulative"]),
|
|
328
|
+
latestValue: z.number().nullable(),
|
|
329
|
+
latestDateKey: z.string().nullable(),
|
|
330
|
+
baselineValue: z.number().nullable(),
|
|
331
|
+
deltaValue: z.number().nullable(),
|
|
332
|
+
coverageDays: z.number().int().nonnegative(),
|
|
333
|
+
days: z.array(psycheMetricDayRecordSchema)
|
|
334
|
+
}))
|
|
335
|
+
});
|
|
277
336
|
export const psycheOverviewPayloadSchema = z.object({
|
|
278
337
|
generatedAt: z.string(),
|
|
279
338
|
domain: domainSchema,
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { scanConversations } from "forge-devrage";
|
|
3
3
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
4
|
+
import { psycheMetricsViewDataSchema } from "../psyche-types.js";
|
|
4
5
|
const SWEAR_COUNT_KEY = "swear_count";
|
|
5
6
|
const SWEARING_MESSAGE_PERCENT_KEY = "swearing_message_percent";
|
|
6
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
|
+
};
|
|
7
24
|
let syncInFlight = null;
|
|
8
25
|
export async function syncDevrageMetricHistory(options = {}) {
|
|
9
26
|
if (syncInFlight) {
|
|
@@ -34,6 +51,7 @@ export function getDevrageMetricPayload() {
|
|
|
34
51
|
const weeklyAverages = getMetricAverages(7);
|
|
35
52
|
return {
|
|
36
53
|
generatedAt,
|
|
54
|
+
hasData: history.some((day) => day.conversationsScanned > 0),
|
|
37
55
|
latestDateKey: latest?.dateKey ?? null,
|
|
38
56
|
rawSwearCount: latest?.rawSwearCount ?? 0,
|
|
39
57
|
swearingMessagePercent: latest?.swearingMessagePercent ?? 0,
|
|
@@ -56,6 +74,153 @@ export function getDevrageMetricPayload() {
|
|
|
56
74
|
}
|
|
57
75
|
};
|
|
58
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
|
+
}
|
|
59
224
|
async function syncDevrageMetricHistoryInternal(options) {
|
|
60
225
|
const dateKey = options.forceFull ? undefined : options.dateKey ?? todayDateKey();
|
|
61
226
|
const report = await scanConversations({
|
|
@@ -201,6 +366,32 @@ function getMetricAverages(days) {
|
|
|
201
366
|
swearingMessagePercent: round(Number(percentAverage) || 0, 1)
|
|
202
367
|
};
|
|
203
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
|
+
}
|
|
204
395
|
function stableId(prefix, ...parts) {
|
|
205
396
|
const digest = createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 20);
|
|
206
397
|
return `${prefix}_${digest}`;
|
|
@@ -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.70",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-openclaw-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.70",
|
|
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": "Apache-2.0",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"follow-redirects": "^1.16.0",
|
|
106
106
|
"hono": "^4.12.18",
|
|
107
107
|
"ip-address": "^10.2.0",
|
|
108
|
+
"ws": "^8.20.1",
|
|
108
109
|
"uuid": "^14.0.0"
|
|
109
110
|
},
|
|
110
111
|
"scripts": {
|
|
@@ -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);
|