forge-openclaw-plugin 0.2.66 → 0.2.68

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/index.html CHANGED
@@ -13,14 +13,14 @@
13
13
  />
14
14
  <link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
15
15
  <link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
16
- <script type="module" crossorigin src="/forge/assets/index-CFZxwOFB.js"></script>
16
+ <script type="module" crossorigin src="/forge/assets/index-BofyMuFh.js"></script>
17
17
  <link rel="modulepreload" crossorigin href="/forge/assets/vendor-BcOHGipZ.js">
18
18
  <link rel="modulepreload" crossorigin href="/forge/assets/board-DFNV9VAZ.js">
19
19
  <link rel="modulepreload" crossorigin href="/forge/assets/ui-g7FaEglG.js">
20
20
  <link rel="modulepreload" crossorigin href="/forge/assets/motion-CXdn34ih.js">
21
21
  <link rel="modulepreload" crossorigin href="/forge/assets/table-CEq3bTDv.js">
22
22
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
23
- <link rel="stylesheet" crossorigin href="/forge/assets/index-lFN9z5op.css">
23
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-B0PIKEnz.css">
24
24
  </head>
25
25
  <body class="bg-canvas text-ink antialiased">
26
26
  <div id="root"></div>
@@ -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
+ );
@@ -46,6 +46,7 @@ import { getInsightsPayload } from "./services/insights.js";
46
46
  import { buildLifeForcePayload, createFatigueSignal, listLifeForceTemplates, resolveLifeForceUser, updateLifeForceProfile, updateLifeForceTemplate } from "./services/life-force.js";
47
47
  import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
48
48
  import { getPsycheOverview } from "./services/psyche.js";
49
+ import { syncDevrageMetricHistoryIfNeeded } from "./services/devrage.js";
49
50
  import { exportPsycheObservationCalendar, getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
50
51
  import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
51
52
  import { createDataBackup, exportData, getDataManagementState, maybeRunAutomaticBackup, restoreDataBackup, scanForDataRecoveryCandidates, switchDataRoot, updateDataManagementSettings } from "./services/data-management.js";
@@ -1957,14 +1958,15 @@ function classifyOnboardingEntity(entityType) {
1957
1958
  entityType === "questionnaire_run" ||
1958
1959
  entityType === "preference_judgment" ||
1959
1960
  entityType === "preference_signal" ||
1960
- entityType === "work_adjustment") {
1961
+ entityType === "work_adjustment" ||
1962
+ entityType === "self_observation") {
1961
1963
  return "action_workflow_entity";
1962
1964
  }
1963
1965
  return "read_model_only_surface";
1964
1966
  }
1965
1967
  function buildPreferredMutationPath(entityType) {
1966
1968
  if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1967
- return "/api/v1/entities/create | /api/v1/entities/update | /api/v1/entities/delete | /api/v1/entities/search";
1969
+ return "/api/v1/entities/create | /api/v1/entities/update | /api/v1/entities/delete | /api/v1/entities/restore | /api/v1/entities/search";
1968
1970
  }
1969
1971
  switch (entityType) {
1970
1972
  case "wiki_page":
@@ -1998,6 +2000,12 @@ function buildPreferredMutationPath(entityType) {
1998
2000
  }
1999
2001
  }
2000
2002
  function buildPreferredMutationTool(entityType) {
2003
+ if (entityType === "sleep_session") {
2004
+ return "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities | forge_update_sleep_session for reflective enrichment after review";
2005
+ }
2006
+ if (entityType === "workout_session") {
2007
+ return "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities | forge_update_workout_session for reflective enrichment after review";
2008
+ }
2001
2009
  if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
2002
2010
  return "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities";
2003
2011
  }
@@ -2016,6 +2024,8 @@ function buildPreferredMutationTool(entityType) {
2016
2024
  return "forge_submit_preferences_signal";
2017
2025
  case "work_adjustment":
2018
2026
  return "forge_adjust_work_minutes";
2027
+ case "self_observation":
2028
+ return "forge_get_self_observation_calendar | forge_create_entities | forge_update_entities";
2019
2029
  case "movement":
2020
2030
  return "forge_call_movement_route";
2021
2031
  case "life_force":
@@ -3417,10 +3427,11 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3417
3427
  coachingGoal: "Help the user describe how the mode shows up, what it is trying to do, what it fears, and what burden it carries, rather than reducing it to a label only.",
3418
3428
  askSequence: [
3419
3429
  "Start with a recent moment when this part-state took over.",
3420
- "Choose the mode family once the lived description is clearer.",
3421
- "Name the mode in the user's language.",
3422
3430
  "Describe the felt persona, body posture, imagery, or symbolic form.",
3423
- "Clarify its fear, burden, and protective job.",
3431
+ "Clarify what it is trying to protect, prevent, control, or force.",
3432
+ "Clarify its fear, burden, and protective job before choosing a family label.",
3433
+ "Offer one candidate name or formulation in the user's language and invite correction.",
3434
+ "Choose the mode family only after the lived description, protective job, fear, or burden is visible enough.",
3424
3435
  "Explore when it first became necessary or familiar.",
3425
3436
  "Notice linked patterns, behaviors, values, and what a healthy-adult response would need to do."
3426
3437
  ],
@@ -6221,10 +6232,27 @@ export async function buildServer(options = {}) {
6221
6232
  void maybeRunAutomaticBackup().catch(() => {
6222
6233
  // Ignore startup backup failures; the Data settings surface exposes recovery.
6223
6234
  });
6235
+ const devrageMetricSyncEnabled = options.devrageMetricSync ?? !options.dataRoot;
6236
+ const devrageMetricTimer = devrageMetricSyncEnabled
6237
+ ? setInterval(() => {
6238
+ void syncDevrageMetricHistoryIfNeeded().catch(() => {
6239
+ // Devrage is a local metric import; failures should not break Forge.
6240
+ });
6241
+ }, 60 * 60 * 1000)
6242
+ : null;
6243
+ devrageMetricTimer?.unref?.();
6244
+ if (devrageMetricSyncEnabled) {
6245
+ void syncDevrageMetricHistoryIfNeeded().catch(() => {
6246
+ // The Psyche metric payload exposes an empty state until sync succeeds.
6247
+ });
6248
+ }
6224
6249
  app.addHook("onClose", async () => {
6225
6250
  clearInterval(diagnosticRetentionTimer);
6226
6251
  clearInterval(cronSchedulerTimer);
6227
6252
  clearInterval(dataBackupTimer);
6253
+ if (devrageMetricTimer) {
6254
+ clearInterval(devrageMetricTimer);
6255
+ }
6228
6256
  taskRunWatchdog?.stop();
6229
6257
  await stopCompanionIroh();
6230
6258
  await managers.backgroundJobs.stop();
@@ -4645,6 +4645,80 @@ export function buildOpenApiDocument() {
4645
4645
  updatedAt: { type: "string", format: "date-time" }
4646
4646
  }
4647
4647
  };
4648
+ const devrageMetricPayload = {
4649
+ type: "object",
4650
+ additionalProperties: false,
4651
+ required: [
4652
+ "generatedAt",
4653
+ "latestDateKey",
4654
+ "rawSwearCount",
4655
+ "swearingMessagePercent",
4656
+ "conversationsScanned",
4657
+ "messagesScanned",
4658
+ "messagesWithSwears",
4659
+ "dailyAverage",
4660
+ "weeklyAverage",
4661
+ "history",
4662
+ "sync"
4663
+ ],
4664
+ properties: {
4665
+ generatedAt: { type: "string", format: "date-time" },
4666
+ latestDateKey: nullable({ type: "string" }),
4667
+ rawSwearCount: { type: "number" },
4668
+ swearingMessagePercent: { type: "number" },
4669
+ conversationsScanned: { type: "integer" },
4670
+ messagesScanned: { type: "integer" },
4671
+ messagesWithSwears: { type: "integer" },
4672
+ dailyAverage: {
4673
+ type: "object",
4674
+ additionalProperties: false,
4675
+ required: ["rawSwearCount", "swearingMessagePercent"],
4676
+ properties: {
4677
+ rawSwearCount: { type: "number" },
4678
+ swearingMessagePercent: { type: "number" }
4679
+ }
4680
+ },
4681
+ weeklyAverage: {
4682
+ type: "object",
4683
+ additionalProperties: false,
4684
+ required: ["rawSwearCount", "swearingMessagePercent"],
4685
+ properties: {
4686
+ rawSwearCount: { type: "number" },
4687
+ swearingMessagePercent: { type: "number" }
4688
+ }
4689
+ },
4690
+ history: arrayOf({
4691
+ type: "object",
4692
+ additionalProperties: false,
4693
+ required: [
4694
+ "dateKey",
4695
+ "rawSwearCount",
4696
+ "swearingMessagePercent",
4697
+ "conversationsScanned",
4698
+ "messagesScanned",
4699
+ "messagesWithSwears"
4700
+ ],
4701
+ properties: {
4702
+ dateKey: { type: "string" },
4703
+ rawSwearCount: { type: "number" },
4704
+ swearingMessagePercent: { type: "number" },
4705
+ conversationsScanned: { type: "integer" },
4706
+ messagesScanned: { type: "integer" },
4707
+ messagesWithSwears: { type: "integer" }
4708
+ }
4709
+ }),
4710
+ sync: {
4711
+ type: "object",
4712
+ additionalProperties: false,
4713
+ required: ["fullSyncCompletedAt", "lastDailySyncAt", "lastSyncedDateKey"],
4714
+ properties: {
4715
+ fullSyncCompletedAt: nullable({ type: "string", format: "date-time" }),
4716
+ lastDailySyncAt: nullable({ type: "string", format: "date-time" }),
4717
+ lastSyncedDateKey: nullable({ type: "string" })
4718
+ }
4719
+ }
4720
+ }
4721
+ };
4648
4722
  const psycheOverviewPayload = {
4649
4723
  type: "object",
4650
4724
  additionalProperties: false,
@@ -4657,6 +4731,7 @@ export function buildOpenApiDocument() {
4657
4731
  "beliefs",
4658
4732
  "modes",
4659
4733
  "schemaPressure",
4734
+ "devrageMetric",
4660
4735
  "reports",
4661
4736
  "openInsights",
4662
4737
  "openNotes",
@@ -4680,6 +4755,7 @@ export function buildOpenApiDocument() {
4680
4755
  activationCount: { type: "integer" }
4681
4756
  }
4682
4757
  }),
4758
+ devrageMetric: devrageMetricPayload,
4683
4759
  reports: arrayOf({ $ref: "#/components/schemas/TriggerReport" }),
4684
4760
  openInsights: { type: "integer" },
4685
4761
  openNotes: { type: "integer" },
@@ -244,6 +244,36 @@ export const schemaPressureEntrySchema = z.object({
244
244
  title: nonEmptyTrimmedString,
245
245
  activationCount: z.number().int().nonnegative()
246
246
  });
247
+ export const devrageMetricPayloadSchema = z.object({
248
+ generatedAt: z.string(),
249
+ latestDateKey: z.string().nullable(),
250
+ rawSwearCount: z.number().nonnegative(),
251
+ swearingMessagePercent: z.number().nonnegative(),
252
+ conversationsScanned: z.number().int().nonnegative(),
253
+ messagesScanned: z.number().int().nonnegative(),
254
+ messagesWithSwears: z.number().int().nonnegative(),
255
+ dailyAverage: z.object({
256
+ rawSwearCount: z.number().nonnegative(),
257
+ swearingMessagePercent: z.number().nonnegative()
258
+ }),
259
+ weeklyAverage: z.object({
260
+ rawSwearCount: z.number().nonnegative(),
261
+ swearingMessagePercent: z.number().nonnegative()
262
+ }),
263
+ history: z.array(z.object({
264
+ dateKey: z.string(),
265
+ rawSwearCount: z.number().nonnegative(),
266
+ swearingMessagePercent: z.number().nonnegative(),
267
+ conversationsScanned: z.number().int().nonnegative(),
268
+ messagesScanned: z.number().int().nonnegative(),
269
+ messagesWithSwears: z.number().int().nonnegative()
270
+ })),
271
+ sync: z.object({
272
+ fullSyncCompletedAt: z.string().nullable(),
273
+ lastDailySyncAt: z.string().nullable(),
274
+ lastSyncedDateKey: z.string().nullable()
275
+ })
276
+ });
247
277
  export const psycheOverviewPayloadSchema = z.object({
248
278
  generatedAt: z.string(),
249
279
  domain: domainSchema,
@@ -254,6 +284,7 @@ export const psycheOverviewPayloadSchema = z.object({
254
284
  modes: z.array(modeProfileSchema),
255
285
  reports: z.array(triggerReportSchema),
256
286
  schemaPressure: z.array(schemaPressureEntrySchema),
287
+ devrageMetric: devrageMetricPayloadSchema,
257
288
  openInsights: z.number().int().nonnegative(),
258
289
  openNotes: z.number().int().nonnegative(),
259
290
  committedActions: z.array(trimmedString)
@@ -0,0 +1,221 @@
1
+ import { createHash } from "node:crypto";
2
+ import { scanConversations } from "forge-devrage";
3
+ import { getDatabase, runInTransaction } from "../db.js";
4
+ const SWEAR_COUNT_KEY = "swear_count";
5
+ const SWEARING_MESSAGE_PERCENT_KEY = "swearing_message_percent";
6
+ const DEFAULT_ROLE_FILTER = new Set(["user"]);
7
+ let syncInFlight = null;
8
+ export async function syncDevrageMetricHistory(options = {}) {
9
+ if (syncInFlight) {
10
+ return syncInFlight;
11
+ }
12
+ syncInFlight = syncDevrageMetricHistoryInternal(options).finally(() => {
13
+ syncInFlight = null;
14
+ });
15
+ return syncInFlight;
16
+ }
17
+ export async function syncDevrageMetricHistoryIfNeeded() {
18
+ const state = getDevrageSyncState();
19
+ if (!state?.full_sync_completed_at) {
20
+ await syncDevrageMetricHistory({ forceFull: true });
21
+ return;
22
+ }
23
+ const today = todayDateKey();
24
+ if (state.last_synced_date_key !== today) {
25
+ await syncDevrageMetricHistory({ dateKey: today });
26
+ }
27
+ }
28
+ export function getDevrageMetricPayload() {
29
+ const generatedAt = nowIso();
30
+ const state = getDevrageSyncState();
31
+ const history = getDevrageDailyHistory(90);
32
+ const latest = history[0] ?? null;
33
+ const dailyAverages = getMetricAverages();
34
+ const weeklyAverages = getMetricAverages(7);
35
+ return {
36
+ generatedAt,
37
+ latestDateKey: latest?.dateKey ?? null,
38
+ rawSwearCount: latest?.rawSwearCount ?? 0,
39
+ swearingMessagePercent: latest?.swearingMessagePercent ?? 0,
40
+ conversationsScanned: latest?.conversationsScanned ?? 0,
41
+ messagesScanned: latest?.messagesScanned ?? 0,
42
+ messagesWithSwears: latest?.messagesWithSwears ?? 0,
43
+ dailyAverage: {
44
+ rawSwearCount: dailyAverages.rawSwearCount,
45
+ swearingMessagePercent: dailyAverages.swearingMessagePercent
46
+ },
47
+ weeklyAverage: {
48
+ rawSwearCount: weeklyAverages.rawSwearCount,
49
+ swearingMessagePercent: weeklyAverages.swearingMessagePercent
50
+ },
51
+ history,
52
+ sync: {
53
+ fullSyncCompletedAt: state?.full_sync_completed_at ?? null,
54
+ lastDailySyncAt: state?.last_daily_sync_at ?? null,
55
+ lastSyncedDateKey: state?.last_synced_date_key ?? null
56
+ }
57
+ };
58
+ }
59
+ async function syncDevrageMetricHistoryInternal(options) {
60
+ const dateKey = options.forceFull ? undefined : options.dateKey ?? todayDateKey();
61
+ const report = await scanConversations({
62
+ roles: DEFAULT_ROLE_FILTER,
63
+ date: dateKey,
64
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
65
+ });
66
+ storeDevrageReport(report, {
67
+ fullSync: Boolean(options.forceFull),
68
+ syncedDateKey: dateKey ?? null
69
+ });
70
+ }
71
+ export function storeDevrageReport(report, options) {
72
+ const scannedAt = nowIso();
73
+ const affectedDateKeys = new Set(report.conversations.map((conversation) => conversation.dateKey));
74
+ runInTransaction(() => {
75
+ const database = getDatabase();
76
+ const deleteDate = database.prepare(`DELETE FROM psyche_devrage_conversation_measures WHERE date_key = ?`);
77
+ const insertConversation = database.prepare(`INSERT INTO psyche_devrage_conversation_measures (
78
+ id, source, conversation_id, date_key, updated_at, messages,
79
+ messages_with_swears, swear_count, scanned_at
80
+ )
81
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
82
+ ON CONFLICT(source, conversation_id, date_key) DO UPDATE SET
83
+ updated_at = excluded.updated_at,
84
+ messages = excluded.messages,
85
+ messages_with_swears = excluded.messages_with_swears,
86
+ swear_count = excluded.swear_count,
87
+ scanned_at = excluded.scanned_at`);
88
+ for (const dateKey of affectedDateKeys) {
89
+ deleteDate.run(dateKey);
90
+ }
91
+ for (const conversation of report.conversations) {
92
+ 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);
93
+ }
94
+ for (const dateKey of affectedDateKeys) {
95
+ recomputeMetricMeasuresForDate(dateKey, scannedAt);
96
+ }
97
+ upsertSyncState({
98
+ fullSyncCompletedAt: options.fullSync ? scannedAt : undefined,
99
+ lastDailySyncAt: options.fullSync ? undefined : scannedAt,
100
+ lastSyncedDateKey: options.syncedDateKey ?? [...affectedDateKeys].sort().at(-1) ?? null,
101
+ updatedAt: scannedAt
102
+ });
103
+ });
104
+ }
105
+ function recomputeMetricMeasuresForDate(dateKey, computedAt) {
106
+ const aggregate = getDatabase()
107
+ .prepare(`SELECT
108
+ COUNT(*) AS conversations,
109
+ COALESCE(SUM(messages), 0) AS messages,
110
+ COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
111
+ COALESCE(SUM(swear_count), 0) AS swear_count
112
+ FROM psyche_devrage_conversation_measures
113
+ WHERE date_key = ?`)
114
+ .get(dateKey);
115
+ const messages = Number(aggregate.messages) || 0;
116
+ const messagesWithSwears = Number(aggregate.messages_with_swears) || 0;
117
+ const swearCount = Number(aggregate.swear_count) || 0;
118
+ const percent = messages > 0 ? (messagesWithSwears / messages) * 100 : 0;
119
+ upsertMetricMeasure(dateKey, SWEAR_COUNT_KEY, swearCount, "count", Number(aggregate.conversations) || 0, computedAt);
120
+ upsertMetricMeasure(dateKey, SWEARING_MESSAGE_PERCENT_KEY, percent, "percent", messages, computedAt);
121
+ }
122
+ function upsertMetricMeasure(dateKey, metricKey, value, unit, sampleCount, computedAt) {
123
+ getDatabase()
124
+ .prepare(`INSERT INTO psyche_devrage_metric_measures (
125
+ id, date_key, metric_key, value, unit, sample_count, computed_at
126
+ )
127
+ VALUES (?, ?, ?, ?, ?, ?, ?)
128
+ ON CONFLICT(date_key, metric_key) DO UPDATE SET
129
+ value = excluded.value,
130
+ unit = excluded.unit,
131
+ sample_count = excluded.sample_count,
132
+ computed_at = excluded.computed_at`)
133
+ .run(stableId("devrage_metric", dateKey, metricKey), dateKey, metricKey, value, unit, sampleCount, computedAt);
134
+ }
135
+ function upsertSyncState(input) {
136
+ const current = getDevrageSyncState();
137
+ getDatabase()
138
+ .prepare(`INSERT INTO psyche_devrage_sync_state (
139
+ id, full_sync_completed_at, last_daily_sync_at, last_synced_date_key, updated_at
140
+ )
141
+ VALUES ('default', ?, ?, ?, ?)
142
+ ON CONFLICT(id) DO UPDATE SET
143
+ full_sync_completed_at = excluded.full_sync_completed_at,
144
+ last_daily_sync_at = excluded.last_daily_sync_at,
145
+ last_synced_date_key = excluded.last_synced_date_key,
146
+ updated_at = excluded.updated_at`)
147
+ .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);
148
+ }
149
+ function getDevrageSyncState() {
150
+ return (getDatabase()
151
+ .prepare(`SELECT full_sync_completed_at, last_daily_sync_at, last_synced_date_key, updated_at
152
+ FROM psyche_devrage_sync_state
153
+ WHERE id = 'default'`)
154
+ .get() ?? null);
155
+ }
156
+ function getDevrageDailyHistory(limit) {
157
+ const rows = getDatabase()
158
+ .prepare(`SELECT
159
+ date_key,
160
+ COUNT(*) AS conversations,
161
+ COALESCE(SUM(messages), 0) AS messages,
162
+ COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
163
+ COALESCE(SUM(swear_count), 0) AS swear_count
164
+ FROM psyche_devrage_conversation_measures
165
+ GROUP BY date_key
166
+ ORDER BY date_key DESC
167
+ LIMIT ?`)
168
+ .all(limit);
169
+ return rows.map((row) => {
170
+ const messages = Number(row.messages) || 0;
171
+ const messagesWithSwears = Number(row.messages_with_swears) || 0;
172
+ return {
173
+ dateKey: row.date_key,
174
+ rawSwearCount: Number(row.swear_count) || 0,
175
+ swearingMessagePercent: messages > 0 ? (messagesWithSwears / messages) * 100 : 0,
176
+ conversationsScanned: Number(row.conversations) || 0,
177
+ messagesScanned: messages,
178
+ messagesWithSwears
179
+ };
180
+ });
181
+ }
182
+ function getMetricAverages(days) {
183
+ const where = days
184
+ ? `WHERE date_key IN (
185
+ SELECT date_key FROM psyche_devrage_metric_measures
186
+ GROUP BY date_key
187
+ ORDER BY date_key DESC
188
+ LIMIT ?
189
+ )`
190
+ : "";
191
+ const rows = getDatabase()
192
+ .prepare(`SELECT metric_key, AVG(value) AS value
193
+ FROM psyche_devrage_metric_measures
194
+ ${where}
195
+ GROUP BY metric_key`)
196
+ .all(...(days ? [days] : []));
197
+ const swearAverage = rows.find((row) => row.metric_key === SWEAR_COUNT_KEY)?.value ?? 0;
198
+ const percentAverage = rows.find((row) => row.metric_key === SWEARING_MESSAGE_PERCENT_KEY)?.value ?? 0;
199
+ return {
200
+ rawSwearCount: round(Number(swearAverage) || 0, 1),
201
+ swearingMessagePercent: round(Number(percentAverage) || 0, 1)
202
+ };
203
+ }
204
+ function stableId(prefix, ...parts) {
205
+ const digest = createHash("sha256").update(parts.join("\u0000")).digest("hex").slice(0, 20);
206
+ return `${prefix}_${digest}`;
207
+ }
208
+ function todayDateKey() {
209
+ return new Intl.DateTimeFormat("en-CA", {
210
+ year: "numeric",
211
+ month: "2-digit",
212
+ day: "2-digit"
213
+ }).format(new Date());
214
+ }
215
+ function nowIso() {
216
+ return new Date().toISOString();
217
+ }
218
+ function round(value, digits) {
219
+ const factor = 10 ** digits;
220
+ return Math.round(value * factor) / factor;
221
+ }
@@ -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
@@ -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.66",
5
+ "version": "0.2.68",
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.66",
3
+ "version": "0.2.68",
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",
@@ -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
+ );