forge-openclaw-plugin 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-ta0rUHOf.js → board-C6jCchjI.js} +2 -2
  3. package/dist/assets/{board-ta0rUHOf.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-fBKPB6yw.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-fBKPB6yw.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-C-IGTQni.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-C-IGTQni.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-DInOpaYF.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-DInOpaYF.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -7
  18. package/dist/openclaw/local-runtime.js +16 -0
  19. package/dist/openclaw/routes.d.ts +27 -0
  20. package/dist/openclaw/routes.js +16 -12
  21. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  22. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  23. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  24. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  25. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  26. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  27. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  28. package/dist/server/server/src/app.js +1684 -117
  29. package/dist/server/server/src/connectors/box-registry.js +44 -9
  30. package/dist/server/server/src/data-management-types.js +107 -0
  31. package/dist/server/server/src/db.js +68 -4
  32. package/dist/server/server/src/demo-data.js +2 -2
  33. package/dist/server/server/src/health.js +702 -18
  34. package/dist/server/server/src/managers/platform/llm-manager.js +7 -4
  35. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  36. package/dist/server/server/src/managers/platform/secrets-manager.js +18 -1
  37. package/dist/server/server/src/managers/runtime.js +9 -0
  38. package/dist/server/server/src/movement.js +1971 -112
  39. package/dist/server/server/src/openapi.js +489 -1
  40. package/dist/server/server/src/psyche-types.js +9 -1
  41. package/dist/server/server/src/repositories/activity-events.js +8 -0
  42. package/dist/server/server/src/repositories/ai-connectors.js +522 -74
  43. package/dist/server/server/src/repositories/habits.js +37 -1
  44. package/dist/server/server/src/repositories/model-settings.js +13 -3
  45. package/dist/server/server/src/repositories/notes.js +3 -0
  46. package/dist/server/server/src/repositories/settings.js +380 -18
  47. package/dist/server/server/src/repositories/tasks.js +170 -10
  48. package/dist/server/server/src/runtime-data-root.js +82 -0
  49. package/dist/server/server/src/screen-time.js +802 -0
  50. package/dist/server/server/src/services/data-management.js +788 -0
  51. package/dist/server/server/src/services/entity-crud.js +205 -2
  52. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  53. package/dist/server/server/src/services/life-force-model.js +197 -0
  54. package/dist/server/server/src/services/life-force.js +1270 -0
  55. package/dist/server/server/src/services/psyche-observation-calendar.js +383 -16
  56. package/dist/server/server/src/types.js +286 -13
  57. package/dist/server/server/src/web.js +228 -13
  58. package/dist/server/src/components/customization/utility-widgets.js +136 -27
  59. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  60. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  61. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  62. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  63. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +63 -8
  64. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  65. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +62 -54
  66. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +18 -8
  67. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +56 -38
  68. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  69. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  70. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +35 -30
  71. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  72. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  73. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  74. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +3 -1
  75. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +39 -3
  76. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  77. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  78. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +47 -32
  79. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  80. package/dist/server/src/lib/api.js +280 -21
  81. package/dist/server/src/lib/data-management-types.js +1 -0
  82. package/dist/server/src/lib/entity-visuals.js +279 -0
  83. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  84. package/dist/server/src/lib/knowledge-graph.js +470 -0
  85. package/dist/server/src/lib/schemas.js +4 -0
  86. package/dist/server/src/lib/snapshot-normalizer.js +43 -1
  87. package/dist/server/src/lib/workbench/contracts.js +229 -0
  88. package/dist/server/src/lib/workbench/nodes.js +200 -0
  89. package/dist/server/src/lib/workbench/registry.js +52 -5
  90. package/dist/server/src/lib/workbench/runtime.js +254 -38
  91. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  92. package/openclaw.plugin.json +1 -1
  93. package/package.json +1 -1
  94. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  95. package/server/migrations/038_data_management_settings.sql +11 -0
  96. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  97. package/server/migrations/040_screen_time_domain.sql +89 -0
  98. package/server/migrations/041_companion_source_states.sql +21 -0
  99. package/server/migrations/042_movement_boxes.sql +47 -0
  100. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  101. package/skills/forge-openclaw/SKILL.md +24 -11
  102. package/skills/forge-openclaw/entity_conversation_playbooks.md +210 -34
  103. package/skills/forge-openclaw/psyche_entity_playbooks.md +113 -17
  104. package/dist/assets/index-Ro0ZF_az.css +0 -1
  105. package/dist/assets/index-ytlpSj23.js +0 -79
  106. package/dist/assets/index-ytlpSj23.js.map +0 -1
  107. package/dist/assets/vendor-lE3tZJcC.js +0 -876
  108. package/dist/assets/vendor-lE3tZJcC.js.map +0 -1
@@ -225,6 +225,39 @@ function getHabitRow(habitId) {
225
225
  WHERE id = ?`)
226
226
  .get(habitId);
227
227
  }
228
+ function compareDateDesc(left, right) {
229
+ return new Date(right ?? 0).getTime() - new Date(left ?? 0).getTime();
230
+ }
231
+ function compareDateAsc(left, right) {
232
+ return new Date(left ?? 0).getTime() - new Date(right ?? 0).getTime();
233
+ }
234
+ function sortHabits(habits, orderBy) {
235
+ const nextHabits = [...habits];
236
+ nextHabits.sort((left, right) => {
237
+ if (orderBy === "name") {
238
+ return (left.title.localeCompare(right.title, undefined, { sensitivity: "base" }) ||
239
+ compareDateDesc(left.createdAt, right.createdAt));
240
+ }
241
+ if (orderBy === "streak") {
242
+ return (right.streakCount - left.streakCount ||
243
+ Number(right.dueToday) - Number(left.dueToday) ||
244
+ left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
245
+ }
246
+ if (orderBy === "created_at") {
247
+ return (compareDateDesc(left.createdAt, right.createdAt) ||
248
+ left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
249
+ }
250
+ if (orderBy === "updated_at") {
251
+ return (compareDateDesc(left.updatedAt, right.updatedAt) ||
252
+ left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
253
+ }
254
+ return (Number(right.dueToday) - Number(left.dueToday) ||
255
+ compareDateAsc(left.lastCheckInAt, right.lastCheckInAt) ||
256
+ compareDateDesc(left.updatedAt, right.updatedAt) ||
257
+ left.title.localeCompare(right.title, undefined, { sensitivity: "base" }));
258
+ });
259
+ return nextHabits;
260
+ }
228
261
  export function listHabits(filters = {}) {
229
262
  const parsed = filters;
230
263
  const whereClauses = [];
@@ -257,7 +290,10 @@ export function listHabits(filters = {}) {
257
290
  ${limitSql}`)
258
291
  .all(...params);
259
292
  const habits = filterDeletedEntities("habit", rows.map((row) => mapHabit(row)));
260
- return parsed.dueToday ? habits.filter((habit) => habit.dueToday) : habits;
293
+ const filteredHabits = parsed.dueToday
294
+ ? habits.filter((habit) => habit.dueToday)
295
+ : habits;
296
+ return sortHabits(filteredHabits, parsed.orderBy);
261
297
  }
262
298
  export function getHabitById(habitId) {
263
299
  if (isEntityDeleted("habit", habitId)) {
@@ -5,6 +5,7 @@ import { deleteEncryptedSecret, readEncryptedSecret, storeEncryptedSecret } from
5
5
  import { upsertWikiLlmProfile } from "./wiki-memory.js";
6
6
  export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
7
7
  export const DEFAULT_OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
8
+ export const DEFAULT_MOCK_LLM_BASE_URL = "mock://workbench";
8
9
  export const FORGE_MANAGED_WIKI_PROFILE_ID = "wiki_llm_forge_managed";
9
10
  export const FORGE_DEFAULT_AGENT_ID = "agt_forge_default";
10
11
  function parseMetadata(value) {
@@ -26,6 +27,9 @@ export function defaultBaseUrlForProvider(provider) {
26
27
  if (provider === "openai-compatible") {
27
28
  return "http://127.0.0.1:11434/v1";
28
29
  }
30
+ if (provider === "mock") {
31
+ return DEFAULT_MOCK_LLM_BASE_URL;
32
+ }
29
33
  return DEFAULT_OPENAI_BASE_URL;
30
34
  }
31
35
  function buildConnectionAgentId(connectionId) {
@@ -36,7 +40,9 @@ export function buildConnectionAgentIdentity(connection) {
36
40
  ? "Chat agent backed by OpenAI Codex OAuth."
37
41
  : connection.provider === "openai-compatible"
38
42
  ? "Chat agent backed by a local or OpenAI-compatible endpoint."
39
- : "Chat agent backed by the OpenAI API.";
43
+ : connection.provider === "mock"
44
+ ? "Chat agent backed by Forge's deterministic mock workflow runtime."
45
+ : "Chat agent backed by the OpenAI API.";
40
46
  return {
41
47
  id: connection.agentId,
42
48
  label: connection.agentLabel,
@@ -52,7 +58,8 @@ export function buildConnectionAgentIdentity(connection) {
52
58
  };
53
59
  }
54
60
  function mapConnection(row) {
55
- const hasStoredCredential = Boolean(row.secret_id) && Boolean(readEncryptedSecret(row.secret_id));
61
+ const hasStoredCredential = row.provider === "mock" ||
62
+ (Boolean(row.secret_id) && Boolean(readEncryptedSecret(row.secret_id)));
56
63
  return aiModelConnectionSchema.parse({
57
64
  id: row.id,
58
65
  label: row.label,
@@ -122,7 +129,10 @@ export function upsertAiModelConnection(input, secrets, options = {}) {
122
129
  defaultBaseUrlForProvider(provider);
123
130
  let secretId = existing?.secret_id ?? null;
124
131
  let accountLabel = existing?.account_label ?? null;
125
- if (parsed.apiKey?.trim()) {
132
+ if (parsed.provider === "mock") {
133
+ secretId = null;
134
+ }
135
+ else if (parsed.apiKey?.trim()) {
126
136
  secretId =
127
137
  secretId ?? `mdl_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
128
138
  storeEncryptedSecret(secretId, secrets.sealJson({
@@ -10,6 +10,9 @@ function normalizeAnchorKey(anchorKey) {
10
10
  return anchorKey.trim().length > 0 ? anchorKey : null;
11
11
  }
12
12
  function normalizeLinks(links) {
13
+ if (!links) {
14
+ return [];
15
+ }
13
16
  const seen = new Set();
14
17
  return links.filter((link) => {
15
18
  const key = `${link.entityType}:${link.entityId}:${link.anchorKey ?? ""}`;
@@ -1,11 +1,23 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { getDatabase, runInTransaction } from "../db.js";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getDatabase, getEffectiveDataRoot, runInTransaction } from "../db.js";
3
5
  import { logForgeDebug } from "../debug.js";
4
6
  import { recordActivityEvent } from "./activity-events.js";
5
7
  import { recordEventLog } from "./event-log.js";
6
8
  import { resolveGoogleCalendarOauthPublicConfig } from "../services/google-calendar-oauth-config.js";
7
9
  import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
8
10
  import { createAgentTokenSchema, agentIdentitySchema, customThemeSchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
11
+ const settingsFileSchema = settingsPayloadSchema.deepPartial();
12
+ let settingsFileSyncDepth = 0;
13
+ let lastSettingsFileStatus = {
14
+ path: path.join(getEffectiveDataRoot(), "forge.json"),
15
+ exists: false,
16
+ valid: false,
17
+ syncState: "uninitialized",
18
+ parseError: null,
19
+ overrideKeys: []
20
+ };
9
21
  function boolFromInt(value) {
10
22
  return value === 1;
11
23
  }
@@ -25,7 +37,9 @@ function normalizeMicrosoftTenantId(value) {
25
37
  }
26
38
  function normalizeMicrosoftRedirectUri(value) {
27
39
  const trimmed = value?.trim();
28
- return trimmed && trimmed.length > 0 ? trimmed : defaultMicrosoftRedirectUri();
40
+ return trimmed && trimmed.length > 0
41
+ ? trimmed
42
+ : defaultMicrosoftRedirectUri();
29
43
  }
30
44
  function logCalendarSettingsDebug(message, details) {
31
45
  const serialized = Object.entries(details)
@@ -52,6 +66,237 @@ function parseCustomThemeJson(raw) {
52
66
  return null;
53
67
  }
54
68
  }
69
+ function getForgeSettingsFilePath() {
70
+ return path.join(getEffectiveDataRoot(), "forge.json");
71
+ }
72
+ function writeForgeSettingsFileSnapshot(payload) {
73
+ const filePath = getForgeSettingsFilePath();
74
+ mkdirSync(path.dirname(filePath), { recursive: true });
75
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
76
+ lastSettingsFileStatus = {
77
+ path: filePath,
78
+ exists: true,
79
+ valid: true,
80
+ syncState: "mirrored_from_database",
81
+ parseError: null,
82
+ overrideKeys: []
83
+ };
84
+ }
85
+ function readForgeSettingsFile() {
86
+ const filePath = getForgeSettingsFilePath();
87
+ if (!existsSync(filePath)) {
88
+ return {
89
+ filePath,
90
+ exists: false,
91
+ valid: false,
92
+ settings: null,
93
+ parseError: null
94
+ };
95
+ }
96
+ try {
97
+ const raw = readFileSync(filePath, "utf8");
98
+ const parsed = JSON.parse(raw);
99
+ const settings = settingsFileSchema.parse(parsed);
100
+ return {
101
+ filePath,
102
+ exists: true,
103
+ valid: true,
104
+ settings,
105
+ parseError: null
106
+ };
107
+ }
108
+ catch (error) {
109
+ return {
110
+ filePath,
111
+ exists: true,
112
+ valid: false,
113
+ settings: null,
114
+ parseError: error instanceof Error ? error.message : String(error)
115
+ };
116
+ }
117
+ }
118
+ function toSettingsFileOverrideInput(input) {
119
+ const next = {};
120
+ if (input.profile) {
121
+ next.profile = {};
122
+ if (input.profile.operatorName !== undefined) {
123
+ next.profile.operatorName = input.profile.operatorName;
124
+ }
125
+ if (input.profile.operatorEmail !== undefined) {
126
+ next.profile.operatorEmail = input.profile.operatorEmail;
127
+ }
128
+ if (input.profile.operatorTitle !== undefined) {
129
+ next.profile.operatorTitle = input.profile.operatorTitle;
130
+ }
131
+ if (Object.keys(next.profile).length === 0) {
132
+ delete next.profile;
133
+ }
134
+ }
135
+ if (input.notifications) {
136
+ next.notifications = {};
137
+ if (input.notifications.goalDriftAlerts !== undefined) {
138
+ next.notifications.goalDriftAlerts = input.notifications.goalDriftAlerts;
139
+ }
140
+ if (input.notifications.dailyQuestReminders !== undefined) {
141
+ next.notifications.dailyQuestReminders =
142
+ input.notifications.dailyQuestReminders;
143
+ }
144
+ if (input.notifications.achievementCelebrations !== undefined) {
145
+ next.notifications.achievementCelebrations =
146
+ input.notifications.achievementCelebrations;
147
+ }
148
+ if (Object.keys(next.notifications).length === 0) {
149
+ delete next.notifications;
150
+ }
151
+ }
152
+ if (input.execution) {
153
+ next.execution = {};
154
+ if (input.execution.maxActiveTasks !== undefined) {
155
+ next.execution.maxActiveTasks = input.execution.maxActiveTasks;
156
+ }
157
+ if (input.execution.timeAccountingMode !== undefined) {
158
+ next.execution.timeAccountingMode = input.execution.timeAccountingMode;
159
+ }
160
+ if (Object.keys(next.execution).length === 0) {
161
+ delete next.execution;
162
+ }
163
+ }
164
+ if (input.themePreference !== undefined) {
165
+ next.themePreference = input.themePreference;
166
+ }
167
+ if (input.customTheme !== undefined) {
168
+ next.customTheme = input.customTheme;
169
+ }
170
+ if (input.localePreference !== undefined) {
171
+ next.localePreference = input.localePreference;
172
+ }
173
+ if (input.security?.psycheAuthRequired !== undefined) {
174
+ next.security = {
175
+ psycheAuthRequired: input.security.psycheAuthRequired
176
+ };
177
+ }
178
+ if (input.calendarProviders) {
179
+ next.calendarProviders = {};
180
+ if (input.calendarProviders.google) {
181
+ next.calendarProviders.google = {};
182
+ if (input.calendarProviders.google.clientId !== undefined) {
183
+ next.calendarProviders.google.clientId =
184
+ input.calendarProviders.google.clientId;
185
+ }
186
+ if (input.calendarProviders.google.clientSecret !== undefined) {
187
+ next.calendarProviders.google.clientSecret =
188
+ input.calendarProviders.google.clientSecret;
189
+ }
190
+ if (Object.keys(next.calendarProviders.google).length === 0) {
191
+ delete next.calendarProviders.google;
192
+ }
193
+ }
194
+ if (input.calendarProviders.microsoft) {
195
+ next.calendarProviders.microsoft = {};
196
+ if (input.calendarProviders.microsoft.clientId !== undefined) {
197
+ next.calendarProviders.microsoft.clientId =
198
+ input.calendarProviders.microsoft.clientId;
199
+ }
200
+ if (input.calendarProviders.microsoft.tenantId !== undefined) {
201
+ next.calendarProviders.microsoft.tenantId =
202
+ input.calendarProviders.microsoft.tenantId;
203
+ }
204
+ if (input.calendarProviders.microsoft.redirectUri !== undefined) {
205
+ next.calendarProviders.microsoft.redirectUri =
206
+ input.calendarProviders.microsoft.redirectUri;
207
+ }
208
+ if (Object.keys(next.calendarProviders.microsoft).length === 0) {
209
+ delete next.calendarProviders.microsoft;
210
+ }
211
+ }
212
+ if (Object.keys(next.calendarProviders).length === 0) {
213
+ delete next.calendarProviders;
214
+ }
215
+ }
216
+ if (input.modelSettings?.forgeAgent) {
217
+ next.modelSettings = {
218
+ forgeAgent: {}
219
+ };
220
+ if (input.modelSettings.forgeAgent.basicChat) {
221
+ next.modelSettings.forgeAgent.basicChat = {};
222
+ if (input.modelSettings.forgeAgent.basicChat.connectionId !== undefined) {
223
+ next.modelSettings.forgeAgent.basicChat.connectionId =
224
+ input.modelSettings.forgeAgent.basicChat.connectionId;
225
+ }
226
+ if (input.modelSettings.forgeAgent.basicChat.model !== undefined) {
227
+ next.modelSettings.forgeAgent.basicChat.model =
228
+ input.modelSettings.forgeAgent.basicChat.model;
229
+ }
230
+ if (Object.keys(next.modelSettings.forgeAgent.basicChat).length === 0) {
231
+ delete next.modelSettings.forgeAgent.basicChat;
232
+ }
233
+ }
234
+ if (input.modelSettings.forgeAgent.wiki) {
235
+ next.modelSettings.forgeAgent.wiki = {};
236
+ if (input.modelSettings.forgeAgent.wiki.connectionId !== undefined) {
237
+ next.modelSettings.forgeAgent.wiki.connectionId =
238
+ input.modelSettings.forgeAgent.wiki.connectionId;
239
+ }
240
+ if (input.modelSettings.forgeAgent.wiki.model !== undefined) {
241
+ next.modelSettings.forgeAgent.wiki.model =
242
+ input.modelSettings.forgeAgent.wiki.model;
243
+ }
244
+ if (Object.keys(next.modelSettings.forgeAgent.wiki).length === 0) {
245
+ delete next.modelSettings.forgeAgent.wiki;
246
+ }
247
+ }
248
+ if (Object.keys(next.modelSettings.forgeAgent).length === 0) {
249
+ delete next.modelSettings;
250
+ }
251
+ }
252
+ return next;
253
+ }
254
+ function listOverrideKeys(input) {
255
+ const keys = [];
256
+ const pushNestedKeys = (prefix, value) => {
257
+ for (const [key, nestedValue] of Object.entries(value)) {
258
+ if (nestedValue &&
259
+ typeof nestedValue === "object" &&
260
+ !Array.isArray(nestedValue)) {
261
+ pushNestedKeys(`${prefix}.${key}`, nestedValue);
262
+ }
263
+ else if (nestedValue !== undefined) {
264
+ keys.push(`${prefix}.${key}`);
265
+ }
266
+ }
267
+ };
268
+ for (const [key, value] of Object.entries(input)) {
269
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
270
+ keys.push(key);
271
+ continue;
272
+ }
273
+ pushNestedKeys(key, value);
274
+ }
275
+ return keys.sort();
276
+ }
277
+ function pickComparableOverrideSubset(source, template) {
278
+ const picked = {};
279
+ for (const [key, templateValue] of Object.entries(template)) {
280
+ if (templateValue === undefined) {
281
+ continue;
282
+ }
283
+ const sourceValue = source[key];
284
+ if (templateValue &&
285
+ typeof templateValue === "object" &&
286
+ !Array.isArray(templateValue) &&
287
+ sourceValue &&
288
+ typeof sourceValue === "object" &&
289
+ !Array.isArray(sourceValue)) {
290
+ const nested = pickComparableOverrideSubset(sourceValue, templateValue);
291
+ if (Object.keys(nested).length > 0) {
292
+ picked[key] = nested;
293
+ }
294
+ continue;
295
+ }
296
+ picked[key] = sourceValue;
297
+ }
298
+ return picked;
299
+ }
55
300
  function mapAgent(row) {
56
301
  return agentIdentitySchema.parse({
57
302
  id: row.id,
@@ -218,7 +463,7 @@ export function isPsycheAuthRequired() {
218
463
  .get();
219
464
  return row ? boolFromInt(row.psyche_auth_required) : false;
220
465
  }
221
- export function getSettings() {
466
+ function buildSettingsPayloadFromDatabase() {
222
467
  const row = readSettingsRow();
223
468
  const connections = listAiModelConnections();
224
469
  const googleConfig = resolveGoogleCalendarOauthPublicConfig(process.env, {
@@ -264,7 +509,8 @@ export function getSettings() {
264
509
  lastAuditAt: row.last_audit_at,
265
510
  storageMode: "local-first",
266
511
  activeSessions: 1,
267
- tokenCount: listAgentTokens().filter((token) => token.status === "active").length,
512
+ tokenCount: listAgentTokens().filter((token) => token.status === "active")
513
+ .length,
268
514
  psycheAuthRequired: boolFromInt(row.psyche_auth_required)
269
515
  },
270
516
  calendarProviders: {
@@ -290,14 +536,18 @@ export function getSettings() {
290
536
  connectionLabel: basicChatConnection?.label ?? null,
291
537
  provider: basicChatConnection?.provider ?? null,
292
538
  baseUrl: basicChatConnection?.baseUrl ?? null,
293
- model: row.forge_basic_chat_model?.trim() || basicChatConnection?.model || "gpt-5.4-mini"
539
+ model: row.forge_basic_chat_model?.trim() ||
540
+ basicChatConnection?.model ||
541
+ "gpt-5.4-mini"
294
542
  },
295
543
  wiki: {
296
544
  connectionId: wikiConnection?.id ?? null,
297
545
  connectionLabel: wikiConnection?.label ?? null,
298
546
  provider: wikiConnection?.provider ?? null,
299
547
  baseUrl: wikiConnection?.baseUrl ?? null,
300
- model: row.forge_wiki_model?.trim() || wikiConnection?.model || "gpt-5.4-mini"
548
+ model: row.forge_wiki_model?.trim() ||
549
+ wikiConnection?.model ||
550
+ "gpt-5.4-mini"
301
551
  }
302
552
  },
303
553
  connections,
@@ -313,10 +563,99 @@ export function getSettings() {
313
563
  agentTokens: listAgentTokens()
314
564
  });
315
565
  }
316
- export function updateSettings(input, options = {}) {
566
+ function reconcileSettingsFileWithDatabase(current) {
567
+ const fileState = readForgeSettingsFile();
568
+ if (!fileState.exists) {
569
+ writeForgeSettingsFileSnapshot(current);
570
+ lastSettingsFileStatus = {
571
+ path: fileState.filePath,
572
+ exists: true,
573
+ valid: true,
574
+ syncState: "created_from_database",
575
+ parseError: null,
576
+ overrideKeys: []
577
+ };
578
+ return current;
579
+ }
580
+ if (!fileState.valid || !fileState.settings) {
581
+ lastSettingsFileStatus = {
582
+ path: fileState.filePath,
583
+ exists: true,
584
+ valid: false,
585
+ syncState: "invalid",
586
+ parseError: fileState.parseError,
587
+ overrideKeys: []
588
+ };
589
+ return current;
590
+ }
591
+ const overrideInput = toSettingsFileOverrideInput(fileState.settings);
592
+ const overrideKeys = listOverrideKeys(overrideInput);
593
+ let next = current;
594
+ const currentOverride = pickComparableOverrideSubset(toSettingsFileOverrideInput(current), overrideInput);
595
+ const overridesDiffer = JSON.stringify(currentOverride) !== JSON.stringify(overrideInput);
596
+ if (overrideKeys.length > 0 && overridesDiffer) {
597
+ next = updateSettingsInternal(overrideInput, {
598
+ mirrorSettingsFile: false
599
+ });
600
+ }
601
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
602
+ let syncState = overrideKeys.length > 0 && overridesDiffer
603
+ ? "applied_file_overrides"
604
+ : "up_to_date";
605
+ try {
606
+ const existing = readFileSync(fileState.filePath, "utf8");
607
+ if (existing !== serialized) {
608
+ mkdirSync(path.dirname(fileState.filePath), { recursive: true });
609
+ writeFileSync(fileState.filePath, serialized, "utf8");
610
+ if (syncState !== "applied_file_overrides") {
611
+ syncState = "mirrored_from_database";
612
+ }
613
+ }
614
+ }
615
+ catch {
616
+ mkdirSync(path.dirname(fileState.filePath), { recursive: true });
617
+ writeFileSync(fileState.filePath, serialized, "utf8");
618
+ if (syncState !== "applied_file_overrides") {
619
+ syncState = "mirrored_from_database";
620
+ }
621
+ }
622
+ lastSettingsFileStatus = {
623
+ path: fileState.filePath,
624
+ exists: true,
625
+ valid: true,
626
+ syncState,
627
+ parseError: null,
628
+ overrideKeys
629
+ };
630
+ return next;
631
+ }
632
+ export function getSettingsFileStatus() {
633
+ return {
634
+ ...lastSettingsFileStatus,
635
+ path: getForgeSettingsFilePath()
636
+ };
637
+ }
638
+ export function mirrorSettingsFileFromCurrentState() {
639
+ const current = buildSettingsPayloadFromDatabase();
640
+ writeForgeSettingsFileSnapshot(current);
641
+ return current;
642
+ }
643
+ export function getSettings() {
644
+ if (settingsFileSyncDepth > 0) {
645
+ return buildSettingsPayloadFromDatabase();
646
+ }
647
+ settingsFileSyncDepth += 1;
648
+ try {
649
+ return reconcileSettingsFileWithDatabase(buildSettingsPayloadFromDatabase());
650
+ }
651
+ finally {
652
+ settingsFileSyncDepth = Math.max(0, settingsFileSyncDepth - 1);
653
+ }
654
+ }
655
+ function updateSettingsInternal(input, options = {}) {
317
656
  const parsed = updateSettingsSchema.parse(input);
318
657
  return runInTransaction(() => {
319
- const current = getSettings();
658
+ const current = buildSettingsPayloadFromDatabase();
320
659
  const now = new Date().toISOString();
321
660
  const nextGoogleClientId = parsed.calendarProviders?.google?.clientId?.trim() ??
322
661
  current.calendarProviders.google.storedClientId;
@@ -339,18 +678,25 @@ export function updateSettings(input, options = {}) {
339
678
  operatorTitle: parsed.profile?.operatorTitle ?? current.profile.operatorTitle
340
679
  },
341
680
  notifications: {
342
- goalDriftAlerts: parsed.notifications?.goalDriftAlerts ?? current.notifications.goalDriftAlerts,
343
- dailyQuestReminders: parsed.notifications?.dailyQuestReminders ?? current.notifications.dailyQuestReminders,
344
- achievementCelebrations: parsed.notifications?.achievementCelebrations ?? current.notifications.achievementCelebrations
681
+ goalDriftAlerts: parsed.notifications?.goalDriftAlerts ??
682
+ current.notifications.goalDriftAlerts,
683
+ dailyQuestReminders: parsed.notifications?.dailyQuestReminders ??
684
+ current.notifications.dailyQuestReminders,
685
+ achievementCelebrations: parsed.notifications?.achievementCelebrations ??
686
+ current.notifications.achievementCelebrations
345
687
  },
346
688
  execution: {
347
689
  maxActiveTasks: parsed.execution?.maxActiveTasks ?? current.execution.maxActiveTasks,
348
- timeAccountingMode: parsed.execution?.timeAccountingMode ?? current.execution.timeAccountingMode
690
+ timeAccountingMode: parsed.execution?.timeAccountingMode ??
691
+ current.execution.timeAccountingMode
349
692
  },
350
693
  themePreference: parsed.themePreference ?? current.themePreference,
351
- customTheme: parsed.customTheme === undefined ? (current.customTheme ?? null) : parsed.customTheme,
694
+ customTheme: parsed.customTheme === undefined
695
+ ? (current.customTheme ?? null)
696
+ : parsed.customTheme,
352
697
  localePreference: parsed.localePreference ?? current.localePreference,
353
- psycheAuthRequired: parsed.security?.psycheAuthRequired ?? current.security.psycheAuthRequired,
698
+ psycheAuthRequired: parsed.security?.psycheAuthRequired ??
699
+ current.security.psycheAuthRequired,
354
700
  calendarProviders: {
355
701
  google: resolveGoogleCalendarOauthPublicConfig(process.env, {
356
702
  clientId: nextGoogleClientId,
@@ -371,14 +717,15 @@ export function updateSettings(input, options = {}) {
371
717
  connectionId: parsed.modelSettings?.forgeAgent?.basicChat?.connectionId !==
372
718
  undefined
373
719
  ? normalizeModelConnectionId(parsed.modelSettings.forgeAgent.basicChat.connectionId)
374
- : current.modelSettings.forgeAgent.basicChat.connectionId ?? "",
720
+ : (current.modelSettings.forgeAgent.basicChat.connectionId ??
721
+ ""),
375
722
  model: parsed.modelSettings?.forgeAgent?.basicChat?.model?.trim() ||
376
723
  current.modelSettings.forgeAgent.basicChat.model
377
724
  },
378
725
  wiki: {
379
726
  connectionId: parsed.modelSettings?.forgeAgent?.wiki?.connectionId !== undefined
380
727
  ? normalizeModelConnectionId(parsed.modelSettings.forgeAgent.wiki.connectionId)
381
- : current.modelSettings.forgeAgent.wiki.connectionId ?? "",
728
+ : (current.modelSettings.forgeAgent.wiki.connectionId ?? ""),
382
729
  model: parsed.modelSettings?.forgeAgent?.wiki?.model?.trim() ||
383
730
  current.modelSettings.forgeAgent.wiki.model
384
731
  }
@@ -431,7 +778,17 @@ export function updateSettings(input, options = {}) {
431
778
  }
432
779
  });
433
780
  }
434
- return getSettings();
781
+ const updated = buildSettingsPayloadFromDatabase();
782
+ if (options.mirrorSettingsFile !== false) {
783
+ writeForgeSettingsFileSnapshot(updated);
784
+ }
785
+ return updated;
786
+ });
787
+ }
788
+ export function updateSettings(input, options = {}) {
789
+ return updateSettingsInternal(input, {
790
+ ...options,
791
+ mirrorSettingsFile: true
435
792
  });
436
793
  }
437
794
  export function createAgentToken(input, activity) {
@@ -477,6 +834,7 @@ export function createAgentToken(input, activity) {
477
834
  }
478
835
  });
479
836
  }
837
+ mirrorSettingsFileFromCurrentState();
480
838
  return {
481
839
  token,
482
840
  tokenSummary
@@ -514,6 +872,7 @@ export function rotateAgentToken(tokenId, activity) {
514
872
  source: activity.source
515
873
  });
516
874
  }
875
+ mirrorSettingsFileFromCurrentState();
517
876
  return {
518
877
  token,
519
878
  tokenSummary
@@ -549,6 +908,7 @@ export function revokeAgentToken(tokenId, activity) {
549
908
  source: activity.source
550
909
  });
551
910
  }
911
+ mirrorSettingsFileFromCurrentState();
552
912
  return tokenSummary;
553
913
  });
554
914
  }
@@ -580,6 +940,8 @@ export function verifyAgentToken(token) {
580
940
  if (!row || row.revoked_at) {
581
941
  return null;
582
942
  }
583
- getDatabase().prepare(`UPDATE agent_tokens SET last_used_at = ?, updated_at = ? WHERE id = ?`).run(new Date().toISOString(), new Date().toISOString(), row.id);
943
+ getDatabase()
944
+ .prepare(`UPDATE agent_tokens SET last_used_at = ?, updated_at = ? WHERE id = ?`)
945
+ .run(new Date().toISOString(), new Date().toISOString(), row.id);
584
946
  return mapToken(row);
585
947
  }