forge-openclaw-plugin 0.2.25 → 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.
- package/README.md +59 -3
- package/dist/assets/{board-VmF4FAfr.js → board-C6jCchjI.js} +3 -3
- package/dist/assets/{board-VmF4FAfr.js.map → board-C6jCchjI.js.map} +1 -1
- package/dist/assets/index-DVvS8iiU.css +1 -0
- package/dist/assets/index-zYB-9Dfo.js +85 -0
- package/dist/assets/index-zYB-9Dfo.js.map +1 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
- package/dist/assets/{motion-DvkU14p-.js → motion-DFHrH2rd.js} +2 -2
- package/dist/assets/{motion-DvkU14p-.js.map → motion-DFHrH2rd.js.map} +1 -1
- package/dist/assets/{table-DgiPof9E.js → table-ZL7Di_u3.js} +2 -2
- package/dist/assets/{table-DgiPof9E.js.map → table-ZL7Di_u3.js.map} +1 -1
- package/dist/assets/{ui-nYfoC0Gq.js → ui-CKNPpz7q.js} +2 -2
- package/dist/assets/{ui-nYfoC0Gq.js.map → ui-CKNPpz7q.js.map} +1 -1
- package/dist/assets/vendor-DoNZuFhn.js +1247 -0
- package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
- package/dist/index.html +7 -8
- package/dist/openclaw/local-runtime.d.ts +3 -1
- package/dist/openclaw/local-runtime.js +67 -15
- package/dist/openclaw/plugin-entry-shared.js +24 -2
- package/dist/openclaw/plugin-sdk-types.d.ts +17 -0
- package/dist/openclaw/routes.d.ts +27 -0
- package/dist/openclaw/routes.js +16 -12
- package/dist/openclaw/tools.js +0 -3
- package/dist/server/server/migrations/001_core.sql +411 -0
- package/dist/server/server/migrations/002_psyche.sql +392 -0
- package/dist/server/server/migrations/003_habits.sql +30 -0
- package/dist/server/server/migrations/004_habit_links.sql +8 -0
- package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
- package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
- package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
- package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
- package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
- package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
- package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
- package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/dist/server/server/migrations/016_health_companion.sql +158 -0
- package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/dist/server/server/migrations/017_preferences.sql +131 -0
- package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
- package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
- package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
- package/dist/server/server/migrations/024_questionnaires.sql +96 -0
- package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
- package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
- package/dist/server/server/migrations/027_ai_processors.sql +31 -0
- package/dist/server/server/migrations/028_movement_domain.sql +136 -0
- package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
- package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
- package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
- package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
- package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
- package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
- package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
- package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
- package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
- package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
- package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/dist/server/{app.js → server/src/app.js} +2112 -414
- package/dist/server/server/src/connectors/box-registry.js +223 -0
- package/dist/server/server/src/data-management-types.js +107 -0
- package/dist/server/{db.js → server/src/db.js} +72 -4
- package/dist/server/server/src/debug.js +19 -0
- package/dist/server/{demo-data.js → server/src/demo-data.js} +2 -2
- package/dist/server/{health.js → server/src/health.js} +702 -18
- package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +7 -4
- package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
- package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +18 -1
- package/dist/server/{managers → server/src/managers}/runtime.js +9 -0
- package/dist/server/{movement.js → server/src/movement.js} +1971 -112
- package/dist/server/{openapi.js → server/src/openapi.js} +491 -3
- package/dist/server/{psyche-types.js → server/src/psyche-types.js} +9 -1
- package/dist/server/{repositories → server/src/repositories}/activity-events.js +8 -0
- package/dist/server/{repositories → server/src/repositories}/ai-connectors.js +758 -47
- package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
- package/dist/server/{repositories → server/src/repositories}/habits.js +37 -1
- package/dist/server/{repositories → server/src/repositories}/model-settings.js +13 -3
- package/dist/server/{repositories → server/src/repositories}/notes.js +3 -0
- package/dist/server/{repositories → server/src/repositories}/settings.js +431 -21
- package/dist/server/{repositories → server/src/repositories}/tasks.js +170 -10
- package/dist/server/server/src/runtime-data-root.js +82 -0
- package/dist/server/server/src/screen-time.js +802 -0
- package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
- package/dist/server/server/src/services/data-management.js +788 -0
- package/dist/server/{services → server/src/services}/entity-crud.js +205 -2
- package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
- package/dist/server/server/src/services/knowledge-graph.js +1455 -0
- package/dist/server/server/src/services/life-force-model.js +197 -0
- package/dist/server/server/src/services/life-force.js +1270 -0
- package/dist/server/server/src/services/psyche-observation-calendar.js +413 -0
- package/dist/server/{types.js → server/src/types.js} +420 -29
- package/dist/server/server/src/web.js +332 -0
- package/dist/server/src/components/customization/utility-widgets.js +439 -0
- package/dist/server/src/components/ui/info-tooltip.js +25 -0
- package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/health/health-boxes.js +147 -0
- package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
- package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +136 -0
- package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +47 -0
- package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +132 -0
- package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
- package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
- package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
- package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
- package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +6 -0
- package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +49 -0
- package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
- package/dist/server/src/components/workbench-boxes/today/today-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
- package/dist/server/src/lib/api-error.js +37 -0
- package/dist/server/src/lib/api.js +2118 -0
- package/dist/server/src/lib/calendar-name-deduper.js +144 -0
- package/dist/server/src/lib/data-management-types.js +1 -0
- package/dist/server/src/lib/diagnostics.js +67 -0
- package/dist/server/src/lib/entity-visuals.js +279 -0
- package/dist/server/src/lib/knowledge-graph-types.js +276 -0
- package/dist/server/src/lib/knowledge-graph.js +470 -0
- package/dist/server/src/lib/psyche-types.js +1 -0
- package/dist/server/src/lib/questionnaire-types.js +1 -0
- package/dist/server/src/lib/runtime-paths.js +24 -0
- package/dist/server/src/lib/schemas.js +238 -0
- package/dist/server/src/lib/snapshot-normalizer.js +416 -0
- package/dist/server/src/lib/theme-system.js +319 -0
- package/dist/server/src/lib/types.js +1 -0
- package/dist/server/src/lib/utils.js +22 -0
- package/dist/server/src/lib/workbench/boxes.js +16 -0
- package/dist/server/src/lib/workbench/contracts.js +229 -0
- package/dist/server/src/lib/workbench/nodes.js +215 -0
- package/dist/server/src/lib/workbench/registry.js +120 -0
- package/dist/server/src/lib/workbench/runtime.js +397 -0
- package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/index.js +68 -0
- package/server/migrations/035_google_local_auth_settings.sql +2 -0
- package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
- package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/server/migrations/038_data_management_settings.sql +11 -0
- package/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/server/migrations/040_screen_time_domain.sql +89 -0
- package/server/migrations/041_companion_source_states.sql +21 -0
- package/server/migrations/042_movement_boxes.sql +47 -0
- package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/skills/forge-openclaw/SKILL.md +27 -11
- package/skills/forge-openclaw/entity_conversation_playbooks.md +411 -46
- package/skills/forge-openclaw/psyche_entity_playbooks.md +195 -20
- package/dist/assets/index-CFCKDIMH.js +0 -67
- package/dist/assets/index-CFCKDIMH.js.map +0 -1
- package/dist/assets/index-ZPY6U1TU.css +0 -1
- package/dist/assets/vendor-D9PTEPSB.js +0 -824
- package/dist/assets/vendor-D9PTEPSB.js.map +0 -1
- package/dist/assets/viz-Cqb6s--o.js +0 -34
- package/dist/assets/viz-Cqb6s--o.js.map +0 -1
- package/dist/server/connectors/box-registry.js +0 -257
- package/dist/server/services/psyche-observation-calendar.js +0 -46
- package/dist/server/web.js +0 -98
- /package/dist/server/{discovery-advertiser.js → server/src/discovery-advertiser.js} +0 -0
- /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
- /package/dist/server/{errors.js → server/src/errors.js} +0 -0
- /package/dist/server/{index.js → server/src/index.js} +0 -0
- /package/dist/server/{managers → server/src/managers}/base.js +0 -0
- /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
- /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
- /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
- /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
- /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
- /package/dist/server/{questionnaire-flow.js → server/src/questionnaire-flow.js} +0 -0
- /package/dist/server/{questionnaire-seeds.js → server/src/questionnaire-seeds.js} +0 -0
- /package/dist/server/{questionnaire-types.js → server/src/questionnaire-types.js} +0 -0
- /package/dist/server/{repositories → server/src/repositories}/ai-processors.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/preferences.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/questionnaires.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/surface-layouts.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +0 -0
- /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
- /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
- /package/dist/server/{services → server/src/services}/context.js +0 -0
- /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
- /package/dist/server/{services → server/src/services}/gamification.js +0 -0
- /package/dist/server/{services → server/src/services}/insights.js +0 -0
- /package/dist/server/{services → server/src/services}/openai-codex-oauth.js +0 -0
- /package/dist/server/{services → server/src/services}/projects.js +0 -0
- /package/dist/server/{services → server/src/services}/psyche.js +0 -0
- /package/dist/server/{services → server/src/services}/relations.js +0 -0
- /package/dist/server/{services → server/src/services}/reviews.js +0 -0
- /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
- /package/dist/server/{services → server/src/services}/tagging.js +0 -0
- /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
- /package/dist/server/{services → server/src/services}/work-time.js +0 -0
- /package/dist/server/{watch-mobile.js → server/src/watch-mobile.js} +0 -0
|
@@ -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
|
+
}
|