forge-openclaw-plugin 0.2.26 → 0.2.28

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 (109) hide show
  1. package/README.md +60 -3
  2. package/dist/assets/{board-ta0rUHOf.js → board-DPFvZf-D.js} +2 -2
  3. package/dist/assets/{board-ta0rUHOf.js.map → board-DPFvZf-D.js.map} +1 -1
  4. package/dist/assets/index-Auw3JrdE.css +1 -0
  5. package/dist/assets/index-D1H7myQH.js +85 -0
  6. package/dist/assets/index-D1H7myQH.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-Bvwc85ch.js} +2 -2
  10. package/dist/assets/{motion-fBKPB6yw.js.map → motion-Bvwc85ch.js.map} +1 -1
  11. package/dist/assets/{table-C-IGTQni.js → table-FJQTJvUR.js} +2 -2
  12. package/dist/assets/{table-C-IGTQni.js.map → table-FJQTJvUR.js.map} +1 -1
  13. package/dist/assets/{ui-DInOpaYF.js → ui-GXFcgvSw.js} +2 -2
  14. package/dist/assets/{ui-DInOpaYF.js.map → ui-GXFcgvSw.js.map} +1 -1
  15. package/dist/assets/vendor-Cwf49UMz.js +1247 -0
  16. package/dist/assets/vendor-Cwf49UMz.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 +1900 -91
  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 +1390 -105
  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/calendar.js +151 -0
  44. package/dist/server/server/src/repositories/habits.js +37 -1
  45. package/dist/server/server/src/repositories/model-settings.js +13 -3
  46. package/dist/server/server/src/repositories/notes.js +3 -0
  47. package/dist/server/server/src/repositories/settings.js +380 -18
  48. package/dist/server/server/src/repositories/tasks.js +170 -10
  49. package/dist/server/server/src/runtime-data-root.js +82 -0
  50. package/dist/server/server/src/screen-time.js +802 -0
  51. package/dist/server/server/src/services/data-management.js +788 -0
  52. package/dist/server/server/src/services/entity-crud.js +205 -2
  53. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  54. package/dist/server/server/src/services/life-force-model.js +217 -0
  55. package/dist/server/server/src/services/life-force.js +2506 -0
  56. package/dist/server/server/src/services/psyche-observation-calendar.js +383 -16
  57. package/dist/server/server/src/types.js +307 -14
  58. package/dist/server/server/src/web.js +228 -13
  59. package/dist/server/src/components/customization/utility-widgets.js +136 -27
  60. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  61. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  62. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  63. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  64. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +63 -8
  65. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  66. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +62 -54
  67. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +18 -8
  68. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +56 -38
  69. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  70. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  71. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +35 -30
  72. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  73. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  74. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  75. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +3 -1
  76. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +39 -3
  77. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  78. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  79. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +47 -32
  80. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  81. package/dist/server/src/lib/api.js +280 -21
  82. package/dist/server/src/lib/data-management-types.js +1 -0
  83. package/dist/server/src/lib/entity-visuals.js +279 -0
  84. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  85. package/dist/server/src/lib/knowledge-graph.js +470 -0
  86. package/dist/server/src/lib/schemas.js +4 -0
  87. package/dist/server/src/lib/snapshot-normalizer.js +45 -1
  88. package/dist/server/src/lib/workbench/contracts.js +229 -0
  89. package/dist/server/src/lib/workbench/nodes.js +200 -0
  90. package/dist/server/src/lib/workbench/registry.js +52 -5
  91. package/dist/server/src/lib/workbench/runtime.js +254 -38
  92. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  93. package/openclaw.plugin.json +1 -1
  94. package/package.json +1 -1
  95. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  96. package/server/migrations/038_data_management_settings.sql +11 -0
  97. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  98. package/server/migrations/040_screen_time_domain.sql +89 -0
  99. package/server/migrations/041_companion_source_states.sql +21 -0
  100. package/server/migrations/042_movement_boxes.sql +47 -0
  101. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  102. package/skills/forge-openclaw/SKILL.md +41 -11
  103. package/skills/forge-openclaw/entity_conversation_playbooks.md +448 -34
  104. package/skills/forge-openclaw/psyche_entity_playbooks.md +170 -17
  105. package/dist/assets/index-Ro0ZF_az.css +0 -1
  106. package/dist/assets/index-ytlpSj23.js +0 -79
  107. package/dist/assets/index-ytlpSj23.js.map +0 -1
  108. package/dist/assets/vendor-lE3tZJcC.js +0 -876
  109. package/dist/assets/vendor-lE3tZJcC.js.map +0 -1
@@ -0,0 +1,788 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import AdmZip from "adm-zip";
8
+ import { closeDatabase, configureDatabase, getDatabase, getEffectiveDataRoot, initializeDatabase, resolveDatabasePathForDataRoot } from "../db.js";
9
+ import { HttpError } from "../errors.js";
10
+ import { createDataBackupSchema, dataBackupEntrySchema, dataBackupModeSchema, dataExportFormatSchema, dataExportOptionSchema, dataManagementSettingsSchema, dataManagementStateSchema, dataRecoveryCandidateSchema, dataRuntimeSnapshotSchema, restoreDataBackupSchema, switchDataRootSchema, updateDataManagementSettingsSchema } from "../data-management-types.js";
11
+ import { syncLocalAdapterDataRoots, writeMonorepoPreferredDataRoot } from "../runtime-data-root.js";
12
+ const EXPORT_OPTIONS = [
13
+ {
14
+ format: "sqlite",
15
+ label: "SQLite snapshot",
16
+ description: "A portable SQLite snapshot of the live Forge database.",
17
+ mimeType: "application/vnd.sqlite3",
18
+ extension: "sqlite"
19
+ },
20
+ {
21
+ format: "json",
22
+ label: "JSON bundle",
23
+ description: "All user-visible tables exported as structured JSON.",
24
+ mimeType: "application/json",
25
+ extension: "json"
26
+ },
27
+ {
28
+ format: "csv_bundle",
29
+ label: "CSV bundle",
30
+ description: "A zip archive with one CSV per table for spreadsheet workflows.",
31
+ mimeType: "application/zip",
32
+ extension: "zip"
33
+ },
34
+ {
35
+ format: "schema_sql",
36
+ label: "Schema SQL",
37
+ description: "SQL DDL for the current database structure.",
38
+ mimeType: "application/sql",
39
+ extension: "sql"
40
+ },
41
+ {
42
+ format: "schema_json",
43
+ label: "Schema JSON",
44
+ description: "Structured database schema metadata for tooling and inspection.",
45
+ mimeType: "application/json",
46
+ extension: "json"
47
+ }
48
+ ];
49
+ const SKIP_SCAN_DIRECTORIES = new Set([
50
+ ".git",
51
+ "node_modules",
52
+ "dist",
53
+ "build",
54
+ "coverage",
55
+ ".next",
56
+ "backups"
57
+ ]);
58
+ function nowIso() {
59
+ return new Date().toISOString();
60
+ }
61
+ function expandUserPath(value, baseDir = getEffectiveDataRoot()) {
62
+ const trimmed = value.trim();
63
+ if (!trimmed) {
64
+ return baseDir;
65
+ }
66
+ if (trimmed === "~") {
67
+ return os.homedir();
68
+ }
69
+ if (trimmed.startsWith("~/")) {
70
+ return path.join(os.homedir(), trimmed.slice(2));
71
+ }
72
+ return path.resolve(baseDir, trimmed);
73
+ }
74
+ function getDefaultBackupDirectory(dataRoot = getEffectiveDataRoot()) {
75
+ return path.join(path.resolve(dataRoot), "backups");
76
+ }
77
+ function ensureDataManagementSettingsRow() {
78
+ const now = nowIso();
79
+ const dataRoot = getEffectiveDataRoot();
80
+ const backupDirectory = getDefaultBackupDirectory(dataRoot);
81
+ getDatabase()
82
+ .prepare(`INSERT OR IGNORE INTO data_management_settings (
83
+ id,
84
+ preferred_data_root,
85
+ backup_directory,
86
+ backup_frequency_hours,
87
+ auto_repair_enabled,
88
+ last_auto_backup_at,
89
+ last_manual_backup_at,
90
+ created_at,
91
+ updated_at
92
+ ) VALUES (1, ?, ?, NULL, 1, NULL, NULL, ?, ?)`)
93
+ .run(dataRoot, backupDirectory, now, now);
94
+ }
95
+ function readDataManagementSettingsRow() {
96
+ ensureDataManagementSettingsRow();
97
+ return getDatabase()
98
+ .prepare(`SELECT
99
+ preferred_data_root,
100
+ backup_directory,
101
+ backup_frequency_hours,
102
+ auto_repair_enabled,
103
+ last_auto_backup_at,
104
+ last_manual_backup_at,
105
+ created_at,
106
+ updated_at
107
+ FROM data_management_settings
108
+ WHERE id = 1`)
109
+ .get();
110
+ }
111
+ function writeDataManagementSettingsRow(patch) {
112
+ const current = readDataManagementSettingsRow();
113
+ const next = {
114
+ ...current,
115
+ updated_at: nowIso()
116
+ };
117
+ for (const [key, value] of Object.entries(patch)) {
118
+ if (value !== undefined) {
119
+ next[key] = value;
120
+ }
121
+ }
122
+ getDatabase()
123
+ .prepare(`UPDATE data_management_settings
124
+ SET preferred_data_root = ?,
125
+ backup_directory = ?,
126
+ backup_frequency_hours = ?,
127
+ auto_repair_enabled = ?,
128
+ last_auto_backup_at = ?,
129
+ last_manual_backup_at = ?,
130
+ updated_at = ?
131
+ WHERE id = 1`)
132
+ .run(next.preferred_data_root, next.backup_directory, next.backup_frequency_hours, next.auto_repair_enabled, next.last_auto_backup_at, next.last_manual_backup_at, next.updated_at);
133
+ }
134
+ function resolveCurrentDataManagementSettings() {
135
+ const row = readDataManagementSettingsRow();
136
+ const preferredDataRoot = row.preferred_data_root.trim() || getEffectiveDataRoot();
137
+ const backupDirectory = row.backup_directory.trim() || getDefaultBackupDirectory(preferredDataRoot);
138
+ return dataManagementSettingsSchema.parse({
139
+ preferredDataRoot,
140
+ backupDirectory,
141
+ backupFrequencyHours: row.backup_frequency_hours,
142
+ autoRepairEnabled: row.auto_repair_enabled === 1,
143
+ lastAutoBackupAt: row.last_auto_backup_at,
144
+ lastManualBackupAt: row.last_manual_backup_at
145
+ });
146
+ }
147
+ function quoteSqlString(value) {
148
+ return `'${value.replaceAll("'", "''")}'`;
149
+ }
150
+ function detectLayoutForDatabasePath(databasePath) {
151
+ if (!databasePath) {
152
+ return "missing";
153
+ }
154
+ if (path.basename(path.dirname(databasePath)) === "data") {
155
+ return "legacy";
156
+ }
157
+ return "flat";
158
+ }
159
+ function deriveDataRootFromDatabasePath(databasePath) {
160
+ const layout = detectLayoutForDatabasePath(databasePath);
161
+ if (layout === "legacy") {
162
+ return path.dirname(path.dirname(databasePath));
163
+ }
164
+ return path.dirname(databasePath);
165
+ }
166
+ function emptyCounts() {
167
+ return {
168
+ notes: 0,
169
+ goals: 0,
170
+ projects: 0,
171
+ tasks: 0,
172
+ taskRuns: 0,
173
+ tags: 0
174
+ };
175
+ }
176
+ function countRowsInDatabase(database, table) {
177
+ try {
178
+ const row = database
179
+ .prepare(`SELECT COUNT(*) AS count FROM ${table}`)
180
+ .get();
181
+ return row.count;
182
+ }
183
+ catch {
184
+ return 0;
185
+ }
186
+ }
187
+ function collectCountsFromDatabase(database) {
188
+ return {
189
+ notes: countRowsInDatabase(database, "notes"),
190
+ goals: countRowsInDatabase(database, "goals"),
191
+ projects: countRowsInDatabase(database, "projects"),
192
+ tasks: countRowsInDatabase(database, "tasks"),
193
+ taskRuns: countRowsInDatabase(database, "task_runs"),
194
+ tags: countRowsInDatabase(database, "tags")
195
+ };
196
+ }
197
+ function checkIntegrity(database) {
198
+ try {
199
+ const row = database
200
+ .prepare("PRAGMA quick_check;")
201
+ .get();
202
+ const value = row ? Object.values(row)[0] : "ok";
203
+ return {
204
+ integrityOk: value === "ok",
205
+ integrityMessage: value ?? "ok"
206
+ };
207
+ }
208
+ catch (error) {
209
+ return {
210
+ integrityOk: false,
211
+ integrityMessage: error instanceof Error ? error.message : String(error)
212
+ };
213
+ }
214
+ }
215
+ async function statFileIfExists(filePath) {
216
+ try {
217
+ return await stat(filePath);
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ }
223
+ export async function getCurrentDataRuntimeSnapshot() {
224
+ const dataRoot = getEffectiveDataRoot();
225
+ const databasePath = resolveDatabasePathForDataRoot(dataRoot);
226
+ const databaseStat = await statFileIfExists(databasePath);
227
+ const database = getDatabase();
228
+ const integrity = checkIntegrity(database);
229
+ return dataRuntimeSnapshotSchema.parse({
230
+ dataRoot,
231
+ databasePath,
232
+ layout: databaseStat ? detectLayoutForDatabasePath(databasePath) : "missing",
233
+ databaseSizeBytes: databaseStat?.size ?? 0,
234
+ databaseLastModifiedAt: databaseStat?.mtime.toISOString() ?? null,
235
+ integrityOk: integrity.integrityOk,
236
+ integrityMessage: integrity.integrityMessage,
237
+ counts: collectCountsFromDatabase(database)
238
+ });
239
+ }
240
+ function listTables(database) {
241
+ return database
242
+ .prepare(`SELECT name
243
+ FROM sqlite_schema
244
+ WHERE type = 'table'
245
+ AND name NOT LIKE 'sqlite_%'
246
+ ORDER BY name`)
247
+ .all().map((row) => row.name);
248
+ }
249
+ function buildSchemaSql(database) {
250
+ const rows = database
251
+ .prepare(`SELECT sql
252
+ FROM sqlite_schema
253
+ WHERE sql IS NOT NULL
254
+ ORDER BY
255
+ CASE type
256
+ WHEN 'table' THEN 0
257
+ WHEN 'index' THEN 1
258
+ WHEN 'trigger' THEN 2
259
+ WHEN 'view' THEN 3
260
+ ELSE 4
261
+ END,
262
+ name`)
263
+ .all();
264
+ return rows.map((row) => `${row.sql};`).join("\n\n");
265
+ }
266
+ function buildSchemaJson(database) {
267
+ const tables = listTables(database).map((table) => {
268
+ const columns = database.prepare(`PRAGMA table_info(${quoteSqlString(table)});`).all().map((column) => ({
269
+ cid: column.cid,
270
+ name: column.name,
271
+ type: column.type,
272
+ notNull: column.notnull === 1,
273
+ defaultValue: column.dflt_value,
274
+ primaryKeyPosition: column.pk
275
+ }));
276
+ const foreignKeys = database.prepare(`PRAGMA foreign_key_list(${quoteSqlString(table)});`).all().map((foreignKey) => ({
277
+ id: foreignKey.id,
278
+ sequence: foreignKey.seq,
279
+ table: foreignKey.table,
280
+ from: foreignKey.from,
281
+ to: foreignKey.to,
282
+ onUpdate: foreignKey.on_update,
283
+ onDelete: foreignKey.on_delete
284
+ }));
285
+ const indexes = database.prepare(`PRAGMA index_list(${quoteSqlString(table)});`).all().map((index) => ({
286
+ sequence: index.seq,
287
+ name: index.name,
288
+ unique: index.unique === 1,
289
+ origin: index.origin,
290
+ partial: index.partial === 1,
291
+ columns: database.prepare(`PRAGMA index_info(${quoteSqlString(index.name)});`).all().map((column) => ({
292
+ sequence: column.seqno,
293
+ cid: column.cid,
294
+ name: column.name
295
+ }))
296
+ }));
297
+ return {
298
+ table,
299
+ columns,
300
+ foreignKeys,
301
+ indexes
302
+ };
303
+ });
304
+ return {
305
+ generatedAt: nowIso(),
306
+ tables
307
+ };
308
+ }
309
+ function buildJsonExport(database) {
310
+ const tables = listTables(database);
311
+ const payload = Object.fromEntries(tables.map((table) => {
312
+ const rows = database.prepare(`SELECT * FROM ${table}`).all();
313
+ return [table, rows];
314
+ }));
315
+ return {
316
+ generatedAt: nowIso(),
317
+ tables: payload
318
+ };
319
+ }
320
+ function csvEscape(value) {
321
+ if (value === null || value === undefined) {
322
+ return "";
323
+ }
324
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
325
+ if (/[",\n]/.test(raw)) {
326
+ return `"${raw.replaceAll('"', '""')}"`;
327
+ }
328
+ return raw;
329
+ }
330
+ function buildCsvForTable(database, table) {
331
+ const rows = database.prepare(`SELECT * FROM ${table}`).all();
332
+ if (rows.length === 0) {
333
+ return "";
334
+ }
335
+ const headers = Object.keys(rows[0]);
336
+ return [
337
+ headers.join(","),
338
+ ...rows.map((row) => headers.map((header) => csvEscape(row[header])).join(","))
339
+ ].join("\n");
340
+ }
341
+ async function createSqliteSnapshot(database) {
342
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "forge-sqlite-export-"));
343
+ const snapshotPath = path.join(tempDir, "forge.sqlite");
344
+ database.exec(`VACUUM INTO ${quoteSqlString(snapshotPath)};`);
345
+ return {
346
+ tempDir,
347
+ snapshotPath
348
+ };
349
+ }
350
+ async function removeIfExists(targetPath) {
351
+ try {
352
+ await rm(targetPath, { recursive: true, force: true });
353
+ }
354
+ catch {
355
+ // Ignore cleanup failures for missing files.
356
+ }
357
+ }
358
+ async function copyIfExists(sourcePath, targetPath) {
359
+ if (!existsSync(sourcePath)) {
360
+ return;
361
+ }
362
+ const sourceStat = await stat(sourcePath);
363
+ if (sourceStat.isDirectory()) {
364
+ await cp(sourcePath, targetPath, { recursive: true });
365
+ return;
366
+ }
367
+ await mkdir(path.dirname(targetPath), { recursive: true });
368
+ await cp(sourcePath, targetPath);
369
+ }
370
+ async function checkpointCurrentDatabase() {
371
+ try {
372
+ getDatabase().exec("PRAGMA wal_checkpoint(TRUNCATE);");
373
+ }
374
+ catch {
375
+ // The runtime can continue even if checkpointing fails.
376
+ }
377
+ }
378
+ function buildBackupBaseName(createdAt, id) {
379
+ return `forge-backup-${createdAt.replaceAll(/[:.]/g, "-")}-${id}`;
380
+ }
381
+ function manifestPathForBaseName(backupDirectory, baseName) {
382
+ return path.join(backupDirectory, `${baseName}.manifest.json`);
383
+ }
384
+ function archivePathForBaseName(backupDirectory, baseName) {
385
+ return path.join(backupDirectory, `${baseName}.zip`);
386
+ }
387
+ export async function listDataBackups() {
388
+ const settings = resolveCurrentDataManagementSettings();
389
+ await mkdir(settings.backupDirectory, { recursive: true });
390
+ const entries = await readdir(settings.backupDirectory);
391
+ const manifests = entries
392
+ .filter((entry) => entry.endsWith(".manifest.json"))
393
+ .sort()
394
+ .reverse();
395
+ const backups = [];
396
+ for (const manifestName of manifests) {
397
+ const manifestPath = path.join(settings.backupDirectory, manifestName);
398
+ try {
399
+ const raw = await readFile(manifestPath, "utf8");
400
+ backups.push(dataBackupEntrySchema.parse(JSON.parse(raw)));
401
+ }
402
+ catch {
403
+ // Ignore malformed backup manifests so one bad file does not break the page.
404
+ }
405
+ }
406
+ return backups;
407
+ }
408
+ export async function createDataBackup(input = {}, options = {}) {
409
+ const parsed = createDataBackupSchema.parse(input);
410
+ const mode = dataBackupModeSchema.parse(options.mode ?? "manual");
411
+ const settings = resolveCurrentDataManagementSettings();
412
+ const snapshot = await getCurrentDataRuntimeSnapshot();
413
+ await mkdir(settings.backupDirectory, { recursive: true });
414
+ const backupId = `bkp_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
415
+ const createdAt = nowIso();
416
+ const baseName = buildBackupBaseName(createdAt, backupId);
417
+ const archivePath = archivePathForBaseName(settings.backupDirectory, baseName);
418
+ const manifestPath = manifestPathForBaseName(settings.backupDirectory, baseName);
419
+ const database = getDatabase();
420
+ const sqliteSnapshot = await createSqliteSnapshot(database);
421
+ try {
422
+ const zip = new AdmZip();
423
+ zip.addLocalFile(sqliteSnapshot.snapshotPath, "", "forge.sqlite");
424
+ zip.addFile("schema.sql", Buffer.from(buildSchemaSql(database), "utf8"));
425
+ zip.addFile("schema.json", Buffer.from(JSON.stringify(buildSchemaJson(database), null, 2), "utf8"));
426
+ zip.addFile("snapshot-summary.json", Buffer.from(JSON.stringify({
427
+ generatedAt: createdAt,
428
+ mode,
429
+ note: parsed.note,
430
+ current: snapshot
431
+ }, null, 2), "utf8"));
432
+ const currentRoot = getEffectiveDataRoot();
433
+ const wikiPath = path.join(currentRoot, "wiki");
434
+ if (existsSync(wikiPath)) {
435
+ zip.addLocalFolder(wikiPath, "wiki");
436
+ }
437
+ const wikiIngestPath = path.join(currentRoot, "wiki-ingest");
438
+ if (existsSync(wikiIngestPath)) {
439
+ zip.addLocalFolder(wikiIngestPath, "wiki-ingest");
440
+ }
441
+ const secretsKeyPath = path.join(currentRoot, ".forge-secrets.key");
442
+ if (existsSync(secretsKeyPath)) {
443
+ zip.addLocalFile(secretsKeyPath, "", ".forge-secrets.key");
444
+ }
445
+ zip.writeZip(archivePath);
446
+ const archiveStat = await stat(archivePath);
447
+ const backup = dataBackupEntrySchema.parse({
448
+ id: backupId,
449
+ createdAt,
450
+ mode,
451
+ note: parsed.note,
452
+ sourceDataRoot: currentRoot,
453
+ backupDirectory: settings.backupDirectory,
454
+ archivePath,
455
+ manifestPath,
456
+ databasePath: snapshot.databasePath,
457
+ sizeBytes: archiveStat.size,
458
+ includesWiki: existsSync(wikiPath),
459
+ includesSecretsKey: existsSync(secretsKeyPath),
460
+ counts: snapshot.counts
461
+ });
462
+ await writeFile(manifestPath, `${JSON.stringify(backup, null, 2)}\n`, "utf8");
463
+ if (mode === "manual") {
464
+ writeDataManagementSettingsRow({ last_manual_backup_at: createdAt });
465
+ }
466
+ if (mode === "automatic") {
467
+ writeDataManagementSettingsRow({ last_auto_backup_at: createdAt });
468
+ }
469
+ return backup;
470
+ }
471
+ finally {
472
+ await rm(sqliteSnapshot.tempDir, { recursive: true, force: true });
473
+ }
474
+ }
475
+ async function openDatabaseSnapshot(databasePath) {
476
+ const database = new DatabaseSync(databasePath);
477
+ database.exec("PRAGMA busy_timeout = 250;");
478
+ return database;
479
+ }
480
+ async function inspectDatabaseCandidate(databasePath, current) {
481
+ const dbStat = await statFileIfExists(databasePath);
482
+ if (!dbStat) {
483
+ return null;
484
+ }
485
+ const database = await openDatabaseSnapshot(databasePath);
486
+ try {
487
+ const integrity = checkIntegrity(database);
488
+ const counts = collectCountsFromDatabase(database);
489
+ const dataRoot = deriveDataRootFromDatabasePath(databasePath);
490
+ const sameAsCurrent = path.resolve(dataRoot) === path.resolve(current.dataRoot);
491
+ const sourceHint = dataRoot.includes(`${path.sep}.openclaw${path.sep}`)
492
+ ? "OpenClaw"
493
+ : dataRoot.includes(`${path.sep}.hermes${path.sep}`)
494
+ ? "Hermes"
495
+ : dataRoot.includes(`${path.sep}backups${path.sep}`)
496
+ ? "Backup copy"
497
+ : dataRoot.includes(`${path.sep}projects${path.sep}`)
498
+ ? "Project-local"
499
+ : dataRoot.includes(`${path.sep}data${path.sep}`)
500
+ ? "Shared data"
501
+ : "Disk candidate";
502
+ return dataRecoveryCandidateSchema.parse({
503
+ id: createHash("sha1").update(databasePath).digest("hex").slice(0, 12),
504
+ dataRoot,
505
+ databasePath,
506
+ layout: detectLayoutForDatabasePath(databasePath),
507
+ sourceHint,
508
+ databaseSizeBytes: dbStat.size,
509
+ databaseLastModifiedAt: dbStat.mtime.toISOString(),
510
+ integrityOk: integrity.integrityOk,
511
+ integrityMessage: integrity.integrityMessage,
512
+ counts,
513
+ newerThanCurrent: (current.databaseLastModifiedAt
514
+ ? dbStat.mtime.getTime() > new Date(current.databaseLastModifiedAt).getTime()
515
+ : true) && !sameAsCurrent,
516
+ sameAsCurrent
517
+ });
518
+ }
519
+ finally {
520
+ database.close();
521
+ }
522
+ }
523
+ function gatherScanRoots(explicitRoots) {
524
+ if (explicitRoots && explicitRoots.length > 0) {
525
+ return Array.from(new Set(explicitRoots.map((entry) => path.resolve(entry)))).filter((entry) => existsSync(entry));
526
+ }
527
+ const currentRoot = getEffectiveDataRoot();
528
+ const roots = [
529
+ currentRoot,
530
+ path.dirname(currentRoot),
531
+ process.cwd(),
532
+ path.resolve(process.cwd(), ".."),
533
+ path.join(os.homedir(), ".openclaw"),
534
+ path.join(os.homedir(), ".hermes"),
535
+ path.join(os.homedir(), "Documents")
536
+ ];
537
+ return Array.from(new Set(roots.map((entry) => path.resolve(entry)))).filter((entry) => existsSync(entry));
538
+ }
539
+ function walkForForgeSqlite(rootDir, maxDepth = 5) {
540
+ const matches = [];
541
+ const visit = (dir, depth) => {
542
+ if (depth > maxDepth) {
543
+ return;
544
+ }
545
+ let entries;
546
+ try {
547
+ entries = readdirSync(dir, { withFileTypes: true });
548
+ }
549
+ catch {
550
+ return;
551
+ }
552
+ for (const entry of entries) {
553
+ if (entry.name === "forge.sqlite" && entry.isFile()) {
554
+ matches.push(path.join(dir, entry.name));
555
+ continue;
556
+ }
557
+ if (!entry.isDirectory()) {
558
+ continue;
559
+ }
560
+ if (SKIP_SCAN_DIRECTORIES.has(entry.name)) {
561
+ continue;
562
+ }
563
+ visit(path.join(dir, entry.name), depth + 1);
564
+ }
565
+ };
566
+ visit(rootDir, 0);
567
+ return matches;
568
+ }
569
+ export async function scanForDataRecoveryCandidates(options = {}) {
570
+ const current = await getCurrentDataRuntimeSnapshot();
571
+ const candidates = new Map();
572
+ for (const scanRoot of gatherScanRoots(options.roots)) {
573
+ for (const databasePath of walkForForgeSqlite(scanRoot, options.maxDepth ?? 5)) {
574
+ const candidate = await inspectDatabaseCandidate(databasePath, current);
575
+ if (!candidate) {
576
+ continue;
577
+ }
578
+ if (candidate.counts.notes === 0 && candidate.counts.goals === 0 && candidate.counts.tasks === 0) {
579
+ continue;
580
+ }
581
+ candidates.set(candidate.databasePath, candidate);
582
+ }
583
+ }
584
+ return Array.from(candidates.values()).sort((left, right) => {
585
+ const rightTime = right.databaseLastModifiedAt
586
+ ? new Date(right.databaseLastModifiedAt).getTime()
587
+ : 0;
588
+ const leftTime = left.databaseLastModifiedAt
589
+ ? new Date(left.databaseLastModifiedAt).getTime()
590
+ : 0;
591
+ return rightTime - leftTime;
592
+ });
593
+ }
594
+ function runtimeAssetPaths(dataRoot) {
595
+ const resolvedRoot = path.resolve(dataRoot);
596
+ return {
597
+ dataRoot: resolvedRoot,
598
+ databasePath: resolveDatabasePathForDataRoot(resolvedRoot),
599
+ wikiPath: path.join(resolvedRoot, "wiki"),
600
+ wikiIngestPath: path.join(resolvedRoot, "wiki-ingest"),
601
+ secretsKeyPath: path.join(resolvedRoot, ".forge-secrets.key")
602
+ };
603
+ }
604
+ async function copyRuntimeAssets(sourceRoot, targetRoot) {
605
+ const source = runtimeAssetPaths(sourceRoot);
606
+ const target = runtimeAssetPaths(targetRoot);
607
+ await mkdir(target.dataRoot, { recursive: true });
608
+ if (existsSync(target.databasePath) || existsSync(target.wikiPath) || existsSync(target.secretsKeyPath)) {
609
+ throw new HttpError(409, "target_data_root_not_empty", `Forge found existing runtime data under ${target.dataRoot}. Pick another folder or adopt the existing runtime instead.`);
610
+ }
611
+ await copyIfExists(source.databasePath, target.databasePath);
612
+ await copyIfExists(source.wikiPath, target.wikiPath);
613
+ await copyIfExists(source.wikiIngestPath, target.wikiIngestPath);
614
+ await copyIfExists(source.secretsKeyPath, target.secretsKeyPath);
615
+ }
616
+ async function applyRuntimeRootSwitch(targetDataRoot, secretsManager) {
617
+ closeDatabase();
618
+ configureDatabase({ dataRoot: targetDataRoot });
619
+ await initializeDatabase();
620
+ secretsManager?.configure(targetDataRoot);
621
+ }
622
+ export async function switchDataRoot(input, options = {}) {
623
+ const parsed = switchDataRootSchema.parse(input);
624
+ const currentRoot = getEffectiveDataRoot();
625
+ const previousSettings = resolveCurrentDataManagementSettings();
626
+ const targetDataRoot = expandUserPath(parsed.targetDataRoot, currentRoot);
627
+ if (path.resolve(targetDataRoot) === path.resolve(currentRoot)) {
628
+ return getDataManagementState();
629
+ }
630
+ if (parsed.createSafetyBackup) {
631
+ await createDataBackup({ note: `Safety backup before switching Forge to ${targetDataRoot}` }, { mode: "pre_switch_root" });
632
+ }
633
+ await checkpointCurrentDatabase();
634
+ if (parsed.mode === "migrate_current") {
635
+ await copyRuntimeAssets(currentRoot, targetDataRoot);
636
+ }
637
+ else {
638
+ const existingDatabasePath = resolveDatabasePathForDataRoot(targetDataRoot);
639
+ if (!existsSync(existingDatabasePath)) {
640
+ throw new HttpError(404, "target_data_root_missing", `Forge could not find an existing database under ${targetDataRoot}.`);
641
+ }
642
+ }
643
+ await applyRuntimeRootSwitch(targetDataRoot, options.secretsManager);
644
+ const nextBackupDirectory = path.resolve(previousSettings.backupDirectory) ===
645
+ path.resolve(getDefaultBackupDirectory(currentRoot))
646
+ ? getDefaultBackupDirectory(targetDataRoot)
647
+ : previousSettings.backupDirectory;
648
+ writeDataManagementSettingsRow({
649
+ preferred_data_root: targetDataRoot,
650
+ backup_directory: nextBackupDirectory,
651
+ backup_frequency_hours: previousSettings.backupFrequencyHours,
652
+ auto_repair_enabled: previousSettings.autoRepairEnabled ? 1 : 0
653
+ });
654
+ await (options.persistPreferredDataRoot ?? writeMonorepoPreferredDataRoot)(targetDataRoot);
655
+ await (options.syncAdapterDataRoots ?? syncLocalAdapterDataRoots)(targetDataRoot);
656
+ return getDataManagementState();
657
+ }
658
+ export async function restoreDataBackup(backupId, input, options = {}) {
659
+ const parsed = restoreDataBackupSchema.parse(input);
660
+ const backup = (await listDataBackups()).find((entry) => entry.id === backupId);
661
+ if (!backup) {
662
+ throw new HttpError(404, "backup_not_found", `Forge could not find backup ${backupId}.`);
663
+ }
664
+ if (parsed.createSafetyBackup) {
665
+ await createDataBackup({ note: `Safety backup before restoring ${backup.id}` }, { mode: "pre_restore" });
666
+ }
667
+ const currentRoot = getEffectiveDataRoot();
668
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "forge-restore-"));
669
+ try {
670
+ const zip = new AdmZip(backup.archivePath);
671
+ zip.extractAllTo(tempDir, true);
672
+ const restoredDatabasePath = path.join(tempDir, "forge.sqlite");
673
+ if (!existsSync(restoredDatabasePath)) {
674
+ throw new HttpError(500, "backup_missing_database", "The selected backup archive does not contain a forge.sqlite snapshot.");
675
+ }
676
+ await checkpointCurrentDatabase();
677
+ closeDatabase();
678
+ await removeIfExists(path.join(currentRoot, "forge.sqlite"));
679
+ await removeIfExists(path.join(currentRoot, "forge.sqlite-wal"));
680
+ await removeIfExists(path.join(currentRoot, "forge.sqlite-shm"));
681
+ await removeIfExists(path.join(currentRoot, "wiki"));
682
+ await removeIfExists(path.join(currentRoot, "wiki-ingest"));
683
+ const restoredSecretsPath = path.join(tempDir, ".forge-secrets.key");
684
+ if (existsSync(restoredSecretsPath)) {
685
+ await removeIfExists(path.join(currentRoot, ".forge-secrets.key"));
686
+ }
687
+ await copyIfExists(restoredDatabasePath, path.join(currentRoot, "forge.sqlite"));
688
+ await copyIfExists(path.join(tempDir, "wiki"), path.join(currentRoot, "wiki"));
689
+ await copyIfExists(path.join(tempDir, "wiki-ingest"), path.join(currentRoot, "wiki-ingest"));
690
+ await copyIfExists(restoredSecretsPath, path.join(currentRoot, ".forge-secrets.key"));
691
+ await applyRuntimeRootSwitch(currentRoot, options.secretsManager);
692
+ return getDataManagementState();
693
+ }
694
+ finally {
695
+ await rm(tempDir, { recursive: true, force: true });
696
+ }
697
+ }
698
+ export async function updateDataManagementSettings(input) {
699
+ const parsed = updateDataManagementSettingsSchema.parse(input);
700
+ const currentRoot = getEffectiveDataRoot();
701
+ writeDataManagementSettingsRow({
702
+ backup_directory: parsed.backupDirectory !== undefined
703
+ ? expandUserPath(parsed.backupDirectory, currentRoot)
704
+ : undefined,
705
+ backup_frequency_hours: parsed.backupFrequencyHours !== undefined
706
+ ? parsed.backupFrequencyHours
707
+ : undefined,
708
+ auto_repair_enabled: parsed.autoRepairEnabled !== undefined
709
+ ? parsed.autoRepairEnabled
710
+ ? 1
711
+ : 0
712
+ : undefined
713
+ });
714
+ return resolveCurrentDataManagementSettings();
715
+ }
716
+ export async function getDataManagementState() {
717
+ return dataManagementStateSchema.parse({
718
+ generatedAt: nowIso(),
719
+ current: await getCurrentDataRuntimeSnapshot(),
720
+ settings: resolveCurrentDataManagementSettings(),
721
+ backups: await listDataBackups(),
722
+ exportOptions: EXPORT_OPTIONS.map((entry) => dataExportOptionSchema.parse(entry))
723
+ });
724
+ }
725
+ export async function maybeRunAutomaticBackup() {
726
+ const settings = resolveCurrentDataManagementSettings();
727
+ if (!settings.backupFrequencyHours) {
728
+ return null;
729
+ }
730
+ const lastAuto = settings.lastAutoBackupAt
731
+ ? new Date(settings.lastAutoBackupAt).getTime()
732
+ : 0;
733
+ const dueMs = settings.backupFrequencyHours * 60 * 60 * 1000;
734
+ if (lastAuto !== 0 && Date.now() - lastAuto < dueMs) {
735
+ return null;
736
+ }
737
+ return createDataBackup({ note: "Automatic Forge data backup" }, { mode: "automatic" });
738
+ }
739
+ export async function exportData(format) {
740
+ const parsedFormat = dataExportFormatSchema.parse(format);
741
+ const database = getDatabase();
742
+ const stamp = new Date().toISOString().slice(0, 19).replaceAll(":", "-");
743
+ if (parsedFormat === "sqlite") {
744
+ const snapshot = await createSqliteSnapshot(database);
745
+ try {
746
+ const body = await readFile(snapshot.snapshotPath);
747
+ return {
748
+ body,
749
+ mimeType: "application/vnd.sqlite3",
750
+ fileName: `forge-${stamp}.sqlite`
751
+ };
752
+ }
753
+ finally {
754
+ await rm(snapshot.tempDir, { recursive: true, force: true });
755
+ }
756
+ }
757
+ if (parsedFormat === "schema_sql") {
758
+ return {
759
+ body: Buffer.from(buildSchemaSql(database), "utf8"),
760
+ mimeType: "application/sql",
761
+ fileName: `forge-schema-${stamp}.sql`
762
+ };
763
+ }
764
+ if (parsedFormat === "schema_json") {
765
+ return {
766
+ body: Buffer.from(JSON.stringify(buildSchemaJson(database), null, 2), "utf8"),
767
+ mimeType: "application/json",
768
+ fileName: `forge-schema-${stamp}.json`
769
+ };
770
+ }
771
+ if (parsedFormat === "json") {
772
+ return {
773
+ body: Buffer.from(JSON.stringify(buildJsonExport(database), null, 2), "utf8"),
774
+ mimeType: "application/json",
775
+ fileName: `forge-export-${stamp}.json`
776
+ };
777
+ }
778
+ const zip = new AdmZip();
779
+ for (const table of listTables(database)) {
780
+ zip.addFile(`${table}.csv`, Buffer.from(buildCsvForTable(database, table), "utf8"));
781
+ }
782
+ zip.addFile("schema.json", Buffer.from(JSON.stringify(buildSchemaJson(database), null, 2), "utf8"));
783
+ return {
784
+ body: zip.toBuffer(),
785
+ mimeType: "application/zip",
786
+ fileName: `forge-csv-export-${stamp}.zip`
787
+ };
788
+ }