synabun 2.0.10 → 2.2.0
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/CHANGELOG.md +207 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +22 -0
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +13 -13
- package/hooks/claude-code/pre-websearch.mjs +12 -1
- package/hooks/claude-code/shared.mjs +5 -0
- package/hooks/claude-code/stop.mjs +1 -1
- package/lib/claude-hooks.js +172 -18
- package/lib/client-config-repair.js +30 -17
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +7 -6
- package/lib/first-launch-protection.js +57 -12
- package/lib/mcp-dist.js +176 -0
- package/lib/release-npm-ci.mjs +138 -0
- package/lib/release-publish.mjs +91 -0
- package/lib/release-scan.mjs +50 -0
- package/lib/server-crash-record.js +123 -0
- package/lib/server-exit.js +318 -0
- package/lib/server-supervisor.js +508 -0
- package/lib/start-launcher.js +37 -4
- package/lib/tools-cli.js +45 -0
- package/lib/update-snapshot.js +302 -0
- package/lib/virustotal.mjs +203 -0
- package/lib/windows-crash-event.js +269 -0
- package/mcp-server/dist/http.d.ts +20 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +59 -12
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts +33 -27
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +128 -143
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/preload.js +1 -1
- package/mcp-server/dist/preload.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +81 -28
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +18 -11
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +67 -11
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts +7 -2
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +22 -63
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
- package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-catalog.js +339 -0
- package/mcp-server/dist/services/tool-catalog.js.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
- package/mcp-server/dist/services/tool-list.d.ts +108 -0
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list.js +334 -0
- package/mcp-server/dist/services/tool-list.js.map +1 -0
- package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-runtime.js +400 -0
- package/mcp-server/dist/services/tool-runtime.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +14 -14
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +29 -19
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill-tools.js +642 -0
- package/mcp-server/dist/tools/skill-tools.js.map +1 -0
- package/mcp-server/dist/tools/skill.d.ts +7 -0
- package/mcp-server/dist/tools/skill.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill.js +11 -0
- package/mcp-server/dist/tools/skill.js.map +1 -0
- package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
- package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/tool-groups.js +147 -0
- package/mcp-server/dist/tools/tool-groups.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-format.js +7 -4
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/http.ts +75 -12
- package/mcp-server/src/index.ts +147 -154
- package/mcp-server/src/preload.ts +12 -12
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +81 -21
- package/mcp-server/src/services/server-instructions.ts +29 -64
- package/mcp-server/src/services/tool-catalog.ts +403 -0
- package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
- package/mcp-server/src/services/tool-list.ts +410 -0
- package/mcp-server/src/services/tool-runtime.ts +457 -0
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +29 -20
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +592 -0
- package/mcp-server/src/tools/skill.ts +13 -0
- package/mcp-server/src/tools/tool-groups.ts +164 -0
- package/mcp-server/src/tools/whiteboard-format.ts +9 -3
- package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
- package/neural-interface/i18n/en.json +3953 -228
- package/neural-interface/i18n/pt-BR.json +3901 -179
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +128 -28
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-brains/claude.js +4 -3
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
- package/neural-interface/lib/assistant-brains/codex.js +9 -5
- package/neural-interface/lib/assistant-brains/opencode.js +120 -54
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +81 -15
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +1073 -200
- package/neural-interface/lib/assistant-envelope.js +156 -2
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +93 -19
- package/neural-interface/lib/assistant-plan-permissions.js +25 -9
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
- package/neural-interface/lib/assistant-route-gate.js +6 -4
- package/neural-interface/lib/assistant-router.js +245 -38
- package/neural-interface/lib/assistant-runtime.js +214 -30
- package/neural-interface/lib/assistant-task-prompt.js +87 -21
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +626 -4
- package/neural-interface/lib/backup-zip-reader.js +10 -5
- package/neural-interface/lib/backup-zip-writer.js +257 -0
- package/neural-interface/lib/browser-execution.js +10 -2
- package/neural-interface/lib/browser-guard-cdp.js +84 -0
- package/neural-interface/lib/browser-network-guard.js +476 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +198 -38
- package/neural-interface/lib/claude-agent-bridge.js +11 -2
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +163 -154
- package/neural-interface/lib/cli-resolver.js +13 -3
- package/neural-interface/lib/cli-update-planner.js +52 -41
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +191 -0
- package/neural-interface/lib/confined-fs.js +30 -3
- package/neural-interface/lib/crash-notice-api.js +98 -0
- package/neural-interface/lib/editor-completion-opencode.js +68 -68
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +13 -10
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/http-guards.js +8 -2
- package/neural-interface/lib/install-details.js +4 -1
- package/neural-interface/lib/mcp-agent-servers.js +67 -0
- package/neural-interface/lib/mcp-client-config.js +35 -33
- package/neural-interface/lib/mcp-external-servers.js +332 -0
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +177 -25
- package/neural-interface/lib/native-loop-providers.js +965 -66
- package/neural-interface/lib/native-loop-runtime.js +290 -42
- package/neural-interface/lib/opencode-config.js +557 -0
- package/neural-interface/lib/opencode-db-schema.js +17 -34
- package/neural-interface/lib/opencode-history.js +48 -218
- package/neural-interface/lib/opencode-mcp-ready.js +108 -0
- package/neural-interface/lib/opencode-native-boundary.js +53 -0
- package/neural-interface/lib/opencode-protocol.js +249 -112
- package/neural-interface/lib/opencode-runtime-config.js +32 -22
- package/neural-interface/lib/opencode-serve-records.js +39 -21
- package/neural-interface/lib/opencode-v2-client.js +48 -153
- package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
- package/neural-interface/lib/opencode-worker-data.js +27 -0
- package/neural-interface/lib/opencode2-client.js +73 -37
- package/neural-interface/lib/opencode2-proxy.js +23 -63
- package/neural-interface/lib/opencode2-translate.js +51 -2
- package/neural-interface/lib/owner-authority.js +249 -0
- package/neural-interface/lib/page-session.js +275 -0
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +793 -0
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +20 -2
- package/neural-interface/lib/rulesets/api.js +7 -6
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +180 -36
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +81 -31
- package/neural-interface/lib/session-title-generator.js +58 -36
- package/neural-interface/lib/skills/api-v2.js +495 -0
- package/neural-interface/lib/skills/bundle.js +418 -0
- package/neural-interface/lib/skills/catalog.js +298 -0
- package/neural-interface/lib/skills/creation-api.js +191 -0
- package/neural-interface/lib/skills/creation-brief.js +247 -0
- package/neural-interface/lib/skills/creation-sources.js +285 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +569 -0
- package/neural-interface/lib/skills/digest.js +119 -0
- package/neural-interface/lib/skills/discover.js +111 -0
- package/neural-interface/lib/skills/draft-confinement.js +216 -0
- package/neural-interface/lib/skills/eval-confinement.js +399 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +135 -0
- package/neural-interface/lib/skills/evals-api.js +212 -0
- package/neural-interface/lib/skills/evals.js +1573 -0
- package/neural-interface/lib/skills/export.js +203 -0
- package/neural-interface/lib/skills/frontmatter.js +64 -0
- package/neural-interface/lib/skills/health.js +75 -0
- package/neural-interface/lib/skills/json-edit.js +165 -0
- package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
- package/neural-interface/lib/skills/known-bridges.js +96 -0
- package/neural-interface/lib/skills/library.js +823 -0
- package/neural-interface/lib/skills/local-store.js +21 -0
- package/neural-interface/lib/skills/migration.js +433 -0
- package/neural-interface/lib/skills/operations.js +45 -0
- package/neural-interface/lib/skills/ownership.js +427 -0
- package/neural-interface/lib/skills/packages.js +88 -0
- package/neural-interface/lib/skills/reviews.js +212 -0
- package/neural-interface/lib/skills/revision.js +80 -0
- package/neural-interface/lib/skills/roots.js +269 -0
- package/neural-interface/lib/skills/runtime-checks.js +358 -0
- package/neural-interface/lib/skills/scan-assets.js +252 -0
- package/neural-interface/lib/skills/scan-context.js +26 -0
- package/neural-interface/lib/skills/scan-shell.js +324 -0
- package/neural-interface/lib/skills/scan.js +364 -0
- package/neural-interface/lib/skills/scanners.js +54 -0
- package/neural-interface/lib/skills/service.js +530 -0
- package/neural-interface/lib/skills/telemetry.js +119 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +1268 -0
- package/neural-interface/lib/worker-guard-setting.js +56 -0
- package/neural-interface/lib/worker-mcp-channel.js +175 -0
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +238 -0
- package/neural-interface/lib/worker-sandbox.js +329 -0
- package/neural-interface/package-lock.json +34 -12
- package/neural-interface/package.json +3 -2
- package/neural-interface/public/index.html +3 -1
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +3 -3
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +125 -31
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +1527 -1336
- package/neural-interface/public/shared/app-frame.js +192 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
- package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
- package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +205 -26
- package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
- package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
- package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
- package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
- package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
- package/neural-interface/public/shared/assistant/asst-render.js +445 -82
- package/neural-interface/public/shared/assistant/asst-route.js +304 -53
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
- package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
- package/neural-interface/public/shared/assistant/asst-state.js +87 -24
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/automation-context.js +5 -2
- package/neural-interface/public/shared/cc-dropdown.js +89 -19
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
- package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
- package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +11 -12
- package/neural-interface/public/shared/cli-status.js +11 -10
- package/neural-interface/public/shared/constants.js +5 -4
- package/neural-interface/public/shared/context-menu-areas.js +34 -0
- package/neural-interface/public/shared/context-menu-logic.js +243 -0
- package/neural-interface/public/shared/context-menu-model.js +367 -0
- package/neural-interface/public/shared/context-menu.js +372 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +21 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +30 -15
- package/neural-interface/public/shared/i18n.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
- package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
- package/neural-interface/public/shared/page-session.js +96 -0
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/registry.js +5 -3
- package/neural-interface/public/shared/retired-2d.js +3 -0
- package/neural-interface/public/shared/schedules-view.js +195 -61
- package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
- package/neural-interface/public/shared/settings/settings-data.js +3 -1
- package/neural-interface/public/shared/settings/settings-ia.js +6 -6
- package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
- package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
- package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +222 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
- package/neural-interface/public/shared/skills/ss-doc.js +173 -0
- package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
- package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
- package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
- package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
- package/neural-interface/public/shared/skills/ss-evals.js +417 -0
- package/neural-interface/public/shared/skills/ss-form.js +159 -0
- package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
- package/neural-interface/public/shared/skills/ss-ids.js +32 -0
- package/neural-interface/public/shared/skills/ss-lint.js +149 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
- package/neural-interface/public/shared/skills/ss-resources.js +165 -0
- package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
- package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
- package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
- package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
- package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
- package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
- package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
- package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
- package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
- package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
- package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
- package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
- package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
- package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
- package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
- package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
- package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
- package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
- package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
- package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
- package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
- package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
- package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
- package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
- package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
- package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +1822 -1756
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +55 -0
- package/neural-interface/public/shared/tool-list-storage.js +24 -0
- package/neural-interface/public/shared/tool-list-view.js +112 -0
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/tour/tour-actor.js +268 -0
- package/neural-interface/public/shared/tour/tour-beats.js +336 -0
- package/neural-interface/public/shared/tour/tour-engine.js +884 -0
- package/neural-interface/public/shared/tour/tour-ink.js +390 -0
- package/neural-interface/public/shared/tour/tour-stage.js +440 -0
- package/neural-interface/public/shared/tour/tour-state.js +40 -0
- package/neural-interface/public/shared/tour/tour-tips.js +99 -0
- package/neural-interface/public/shared/tour/tour.css +144 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +111 -140
- package/neural-interface/public/shared/ui-claude-panel.js +376 -82
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-command-runner.js +6 -3
- package/neural-interface/public/shared/ui-context-menu.js +295 -0
- package/neural-interface/public/shared/ui-cost-widget.js +57 -59
- package/neural-interface/public/shared/ui-crash-notice.js +187 -0
- package/neural-interface/public/shared/ui-detail.js +15 -3
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +50 -47
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +33 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +10 -20
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
- package/neural-interface/public/shared/ui-navbar.js +49 -24
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +27 -15
- package/neural-interface/public/shared/ui-settings.js +683 -526
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
- package/neural-interface/public/shared/ui-skills.js +33 -2830
- package/neural-interface/public/shared/ui-styleguide.js +5 -2
- package/neural-interface/public/shared/ui-sync.js +15 -5
- package/neural-interface/public/shared/ui-terminal.js +270 -190
- package/neural-interface/public/shared/ui-tool-updates.js +6 -4
- package/neural-interface/public/shared/ui-tooltip.js +81 -51
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-trash.js +6 -4
- package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
- package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
- package/neural-interface/public/shared/ui-tutorial.css +47 -100
- package/neural-interface/public/shared/ui-tutorial.js +285 -509
- package/neural-interface/public/shared/ui-update.js +38 -2
- package/neural-interface/public/shared/ui-whiteboard.js +312 -59
- package/neural-interface/public/shared/ui-worker-end.js +5 -1
- package/neural-interface/public/shared/ui-workspaces.js +3 -2
- package/neural-interface/public/shared/view-variant.js +66 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
- package/neural-interface/public/shared/whiteboard-list.js +172 -0
- package/neural-interface/public/shared/worker-end.js +10 -3
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/shared/worker-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +2 -2
- package/neural-interface/public/variant/2d/background.js +2 -1
- package/neural-interface/public/variant/2d/context-menu.js +8 -2
- package/neural-interface/public/variant/2d/graph.js +42 -20
- package/neural-interface/public/variant/2d/hulls.js +14 -9
- package/neural-interface/public/variant/2d/lasso.js +11 -7
- package/neural-interface/public/variant/2d/main.js +32 -1
- package/neural-interface/public/variant/2d/minimap.js +11 -13
- package/neural-interface/public/variant/3d/main.js +69 -6
- package/neural-interface/public/variant/3d/map-controls.js +24 -9
- package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat.css +17 -0
- package/neural-interface/public/vendor/caveat/manifest.json +12 -0
- package/neural-interface/public/vendor/yaml/LICENSE +13 -0
- package/neural-interface/public/vendor/yaml/manifest.json +4 -0
- package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
- package/neural-interface/scripts/build-vendor.mjs +32 -0
- package/neural-interface/server.js +4553 -2855
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +34 -12
- package/package.json +17 -3
- package/postinstall.js +9 -4
- package/preuninstall.js +1 -1
- package/setup.js +97 -322
- package/skills/skill-forge/SKILL.md +177 -0
- package/skills/skill-forge/evals/evals.json +54 -0
- package/skills/skill-forge/references/package.md +105 -0
- package/skills/skill-forge/references/review-and-recovery.md +63 -0
- package/skills/skill-forge/references/runtime-profiles.md +20 -0
- package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
- package/skills/synabun/SKILL.md +10 -7
- package/skills/synabun/modules/audit.md +1 -5
- package/skills/synabun/modules/changelog.md +1 -5
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +6 -9
- package/skills/synabun/modules/schedule.md +6 -6
- package/mcp-server/dist/services/profiles.d.ts +0 -120
- package/mcp-server/dist/services/profiles.d.ts.map +0 -1
- package/mcp-server/dist/services/profiles.js +0 -387
- package/mcp-server/dist/services/profiles.js.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
- package/mcp-server/dist/tools/leonardo.d.ts +0 -13
- package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo.js +0 -21
- package/mcp-server/dist/tools/leonardo.js.map +0 -1
- package/mcp-server/dist/tools/profile.d.ts +0 -17
- package/mcp-server/dist/tools/profile.d.ts.map +0 -1
- package/mcp-server/dist/tools/profile.js +0 -114
- package/mcp-server/dist/tools/profile.js.map +0 -1
- package/mcp-server/src/services/profiles.ts +0 -428
- package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
- package/mcp-server/src/tools/leonardo.ts +0 -30
- package/mcp-server/src/tools/profile.ts +0 -122
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
- package/neural-interface/lib/mcp-profile-servers.js +0 -120
- package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
- package/skills/leonardo/SKILL.md +0 -168
- package/skills/leonardo/icon.svg +0 -1
- package/skills/leonardo/modules/browser-guide.md +0 -378
- package/skills/leonardo/modules/image-prompter.md +0 -283
- package/skills/leonardo/modules/model-advisor.md +0 -100
- package/skills/leonardo/modules/prompt-library.md +0 -110
- package/skills/leonardo/modules/style-guide.md +0 -186
- package/skills/leonardo/modules/video-prompter.md +0 -317
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accessSync,
|
|
3
|
+
closeSync,
|
|
3
4
|
copyFileSync,
|
|
4
5
|
constants as FS_CONSTANTS,
|
|
5
6
|
createReadStream,
|
|
@@ -7,7 +8,9 @@ import {
|
|
|
7
8
|
lstatSync,
|
|
8
9
|
mkdtempSync,
|
|
9
10
|
mkdirSync,
|
|
11
|
+
openSync,
|
|
10
12
|
readFileSync,
|
|
13
|
+
readSync,
|
|
11
14
|
readdirSync,
|
|
12
15
|
renameSync,
|
|
13
16
|
rmSync,
|
|
@@ -16,11 +19,15 @@ import {
|
|
|
16
19
|
unlinkSync,
|
|
17
20
|
writeFileSync,
|
|
18
21
|
} from 'node:fs';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
19
23
|
import { createHash } from 'node:crypto';
|
|
20
|
-
import { basename, dirname, extname, join, parse as parsePath, relative, resolve, sep } from 'node:path';
|
|
24
|
+
import { basename, dirname, extname, isAbsolute, join, parse as parsePath, relative, resolve, sep } from 'node:path';
|
|
25
|
+
import * as nodeSqlite from 'node:sqlite';
|
|
21
26
|
import { DatabaseSync } from 'node:sqlite';
|
|
27
|
+
import { Worker } from 'node:worker_threads';
|
|
22
28
|
import archiver from 'archiver';
|
|
23
29
|
import { openZipArchive, readZipEntry, verifyBackupChecksums } from './backup-zip-reader.js';
|
|
30
|
+
import { writeZipArchive } from './backup-zip-writer.js';
|
|
24
31
|
import { LOCK_FILE_NAME } from './rulesets/lock.js';
|
|
25
32
|
|
|
26
33
|
export const BACKUP_MANIFEST_VERSION = 3;
|
|
@@ -31,6 +38,11 @@ export const DEFAULT_BACKUP_RETENTION = Object.freeze({
|
|
|
31
38
|
dailyDays: 30,
|
|
32
39
|
weeklyWeeks: 12,
|
|
33
40
|
maxBytes: 10 * 1024 * 1024 * 1024,
|
|
41
|
+
// How many pre-update/pre-migration snapshots this generic sweep keeps in a shared folder; the
|
|
42
|
+
// dedicated pruner for the updater's own folder (lib/update-snapshot.js prunePreUpdateSnapshots)
|
|
43
|
+
// reads the same retention.preUpdateKeep through the settings mechanism (GET/PUT
|
|
44
|
+
// /api/system/auto-backup), with its own PRE_UPDATE_KEEP_ENV override.
|
|
45
|
+
preUpdateKeep: 1,
|
|
34
46
|
});
|
|
35
47
|
|
|
36
48
|
/** The DATA_HOME folders every backup copies (createVerifiedBackup); DATA_HOME/.env goes too, as a file. */
|
|
@@ -461,19 +473,31 @@ async function writeArchive({ tempPath, files, manifest, level = 6 }) {
|
|
|
461
473
|
* sha256. Streaming matters: reading the archive into memory failed every
|
|
462
474
|
* backup over 2 GiB, which is when a backup matters most.
|
|
463
475
|
*/
|
|
464
|
-
export async function verifyBackupArchive(path) {
|
|
476
|
+
export async function verifyBackupArchive(path, { onBytes = null } = {}) {
|
|
465
477
|
const zip = await openZipArchive(path);
|
|
466
478
|
try {
|
|
467
479
|
const manifestEntry = zip.getEntry(`${BACKUP_PREFIX}/manifest.json`);
|
|
468
480
|
if (!manifestEntry) throw new Error('Backup verification failed: manifest is missing');
|
|
469
481
|
const manifest = JSON.parse((await readZipEntry(zip, manifestEntry)).toString('utf-8'));
|
|
470
|
-
await verifyBackupChecksums(zip, { prefix: BACKUP_PREFIX, manifest });
|
|
482
|
+
await verifyBackupChecksums(zip, { prefix: BACKUP_PREFIX, manifest, onBytes });
|
|
471
483
|
return manifest;
|
|
472
484
|
} finally {
|
|
473
485
|
await zip.close();
|
|
474
486
|
}
|
|
475
487
|
}
|
|
476
488
|
|
|
489
|
+
/** The manifest of a backup archive, read without reading anything else (its central directory and the manifest entry). */
|
|
490
|
+
export async function readBackupArchiveManifest(path) {
|
|
491
|
+
const zip = await openZipArchive(path);
|
|
492
|
+
try {
|
|
493
|
+
const manifestEntry = zip.getEntry(`${BACKUP_PREFIX}/manifest.json`);
|
|
494
|
+
if (!manifestEntry) throw new Error('Backup verification failed: manifest is missing');
|
|
495
|
+
return JSON.parse((await readZipEntry(zip, manifestEntry)).toString('utf-8'));
|
|
496
|
+
} finally {
|
|
497
|
+
await zip.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
477
501
|
/** Create, reopen, checksum-verify, and atomically publish one immutable ZIP. */
|
|
478
502
|
export async function createVerifiedBackup({
|
|
479
503
|
dataHome,
|
|
@@ -635,6 +659,604 @@ export async function createVerifiedBackup({
|
|
|
635
659
|
}
|
|
636
660
|
}
|
|
637
661
|
|
|
662
|
+
// ── The pre-update snapshot: quick at any size ──
|
|
663
|
+
//
|
|
664
|
+
// createVerifiedBackup() is the full backup (scheduled, manual, the npm updater's): VACUUM INTO, an integrity
|
|
665
|
+
// check, deflate, and a re-read through inflate. On a 3.9 GB memory.db that is 81 s on an Apple M-series SSD,
|
|
666
|
+
// and the pre-update snapshot is taken while SynaBun restarts into a new version, with the user waiting.
|
|
667
|
+
// createUpgradeSnapshot() keeps what that snapshot promises (the state as of one commit, every byte in a ZIP the
|
|
668
|
+
// restore paths read, every database copy passing the integrity check the restore runs, read back once and checked
|
|
669
|
+
// before it is published) and drops what made it slow: VACUUM INTO, deflating the database and inflating it again.
|
|
670
|
+
// The integrity check runs on a thread of its own while the archive is written (about 28 s for that 3.9 GB file).
|
|
671
|
+
//
|
|
672
|
+
// The snapshot also records what it was taken from (`inputs` in its manifest): checkUpgradeSnapshotFreshness()
|
|
673
|
+
// tells later, from any process, whether it still stands for the data as it is (lib/update-snapshot.js before the
|
|
674
|
+
// desktop app replaces itself, lib/first-launch-protection.js before the new version's server starts).
|
|
675
|
+
|
|
676
|
+
/** statfs type of APFS (macOS): a file there can be cloned in constant time. */
|
|
677
|
+
const APFS_STATFS_TYPE = 26;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* How `source` can be cloned (copy on write, in constant time) into the folder `targetDir`, or null: APFS on
|
|
681
|
+
* macOS (/bin/cp -c, which is clonefile), a reflink on Linux (FICLONE; a file system without one refuses it, and
|
|
682
|
+
* the copy falls back). Windows has neither. Only within one volume.
|
|
683
|
+
*/
|
|
684
|
+
export function cloneMethodFor(source, targetDir, { platform = process.platform, statfs = statfsSync, stat = statSync } = {}) {
|
|
685
|
+
try {
|
|
686
|
+
if (stat(source).dev !== stat(targetDir).dev) return null;
|
|
687
|
+
if (platform === 'darwin') return Number(statfs(targetDir).type) === APFS_STATFS_TYPE ? 'clone' : null;
|
|
688
|
+
if (platform === 'linux') return 'reflink';
|
|
689
|
+
} catch { /* no clone: SQLite's backup copies */ }
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function cloneFileNow(source, target, method) {
|
|
694
|
+
if (method === 'clone') execFileSync('/bin/cp', ['-c', source, target], { stdio: 'ignore', timeout: 60_000 });
|
|
695
|
+
else copyFileSync(source, target, FS_CONSTANTS.COPYFILE_FICLONE_FORCE);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const SQLITE_HEADER = Buffer.from('SQLite format 3\0', 'latin1');
|
|
699
|
+
|
|
700
|
+
/** Up to `length` bytes from the start of the file at `path`. */
|
|
701
|
+
function readHead(path, length) {
|
|
702
|
+
const fd = openSync(path, 'r');
|
|
703
|
+
try {
|
|
704
|
+
const head = Buffer.alloc(length);
|
|
705
|
+
const read = readSync(fd, head, 0, length, 0);
|
|
706
|
+
return head.subarray(0, read);
|
|
707
|
+
} finally {
|
|
708
|
+
closeSync(fd);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* True when the file at `path` begins with SQLite's 16-byte header: a database, whatever its name says. False for
|
|
714
|
+
* anything else and for a file that is gone (nothing to copy); any other error is thrown.
|
|
715
|
+
*/
|
|
716
|
+
export function isSqliteDatabaseFile(path) {
|
|
717
|
+
try {
|
|
718
|
+
return readHead(path, SQLITE_HEADER.length).equals(SQLITE_HEADER);
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error?.code === 'ENOENT') return false;
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** SHA-256 (hex) of a file, read in one pass on this thread (WAL files: small, and read while a lock is held). */
|
|
726
|
+
function hashFileNow(path) {
|
|
727
|
+
const digest = createHash('sha256');
|
|
728
|
+
const fd = openSync(path, 'r');
|
|
729
|
+
try {
|
|
730
|
+
const buffer = Buffer.allocUnsafe(4 * 1024 * 1024);
|
|
731
|
+
let position = 0;
|
|
732
|
+
for (;;) {
|
|
733
|
+
const read = readSync(fd, buffer, 0, buffer.length, position);
|
|
734
|
+
if (!read) break;
|
|
735
|
+
digest.update(buffer.subarray(0, read));
|
|
736
|
+
position += read;
|
|
737
|
+
}
|
|
738
|
+
} finally {
|
|
739
|
+
closeSync(fd);
|
|
740
|
+
}
|
|
741
|
+
return digest.digest('hex');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* What tells a later reader, in any process, whether the SQLite database at `path` was written since: its file
|
|
746
|
+
* (device, inode, size, modification and change times, and its first 100 bytes, where a rollback-journal database
|
|
747
|
+
* counts every commit) and the content of its WAL, which every commit of a WAL database changes. An empty WAL counts
|
|
748
|
+
* as none: the last connection to close deletes it, and changes nothing else when it is empty. The fingerprint is
|
|
749
|
+
* physical, so a checkpoint changes it without a commit: that costs a needless new snapshot, never a missed write.
|
|
750
|
+
*/
|
|
751
|
+
export function sqliteFingerprint(path) {
|
|
752
|
+
const stat = statSync(path, { bigint: true });
|
|
753
|
+
let wal = null;
|
|
754
|
+
const walPath = `${path}-wal`;
|
|
755
|
+
try {
|
|
756
|
+
const walSize = statSync(walPath).size;
|
|
757
|
+
if (walSize > 0) wal = { size: walSize, sha256: hashFileNow(walPath) };
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
dev: String(stat.dev),
|
|
763
|
+
ino: String(stat.ino),
|
|
764
|
+
size: Number(stat.size),
|
|
765
|
+
mtimeNs: String(stat.mtimeNs),
|
|
766
|
+
ctimeNs: String(stat.ctimeNs),
|
|
767
|
+
head: createHash('sha256').update(readHead(path, 100)).digest('hex'),
|
|
768
|
+
wal,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** True when two fingerprints (sqliteFingerprint) say the database was not written in between. */
|
|
773
|
+
export function sameSqliteFingerprint(a, b) {
|
|
774
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
775
|
+
for (const key of ['dev', 'ino', 'mtimeNs', 'ctimeNs', 'head']) if (String(a[key]) !== String(b[key])) return false;
|
|
776
|
+
if (Number(a.size) !== Number(b.size)) return false;
|
|
777
|
+
if (!a.wal || !b.wal) return !a.wal && !b.wal;
|
|
778
|
+
return Number(a.wal.size) === Number(b.wal.size) && a.wal.sha256 === b.wal.sha256;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* A consistent copy of the SQLite database `sourcePath` at `targetPath`: the database as of one commit, in one
|
|
783
|
+
* self-contained file (journal_mode DELETE, nothing beside it). As fast as the file system allows:
|
|
784
|
+
*
|
|
785
|
+
* clone APFS or a reflink: the database file and its WAL are cloned while this connection holds the write
|
|
786
|
+
* lock (BEGIN IMMEDIATE: a writer waits a moment, readers go on), then the copy folds its WAL in.
|
|
787
|
+
* Constant time whatever the size. Nothing can go missing between the two clones: a checkpoint may
|
|
788
|
+
* still copy frames into the database file, but the WAL keeps every one of them, because no writer
|
|
789
|
+
* can reset it while the lock is held.
|
|
790
|
+
* backup anywhere else (or when the lock is not free within `busyTimeoutMs`), SQLite's online backup in
|
|
791
|
+
* one step: one sequential read and write, from one read transaction, and no writer ever waits.
|
|
792
|
+
* vacuum a Node without the backup API (before 22.16): VACUUM INTO, as the full backups do.
|
|
793
|
+
*
|
|
794
|
+
* First the WAL is emptied when nothing holds an older snapshot (a TRUNCATE checkpoint, waiting a second at most),
|
|
795
|
+
* so the source's fingerprint (sqliteFingerprint) survives the close of its last connection; it is taken under the
|
|
796
|
+
* write lock for a clone, right before the copy otherwise (a commit in between makes a later check find it written:
|
|
797
|
+
* never the other way round). The copy is then opened read-only once: its header must account for its whole size,
|
|
798
|
+
* and the memories of the `primary` database are counted (a query that fails there fails the copy). Resolves
|
|
799
|
+
* { method, path, sizeBytes, memoryCount, pageCount, fingerprint }, or null when there is no database.
|
|
800
|
+
*/
|
|
801
|
+
export async function copySqliteDatabase(sourcePath, targetPath, {
|
|
802
|
+
platform = process.platform, method: forced = null, busyTimeoutMs = 15_000, clone = cloneFileNow, primary = false,
|
|
803
|
+
} = {}) {
|
|
804
|
+
if (!existsSync(sourcePath)) return null;
|
|
805
|
+
for (const suffix of ['', '-wal', '-shm', '-journal']) rmSync(`${targetPath}${suffix}`, { force: true });
|
|
806
|
+
const fallback = typeof nodeSqlite.backup === 'function' ? 'backup' : 'vacuum';
|
|
807
|
+
let method = forced || cloneMethodFor(sourcePath, dirname(targetPath), { platform }) || fallback;
|
|
808
|
+
let fingerprint = null;
|
|
809
|
+
const source = new DatabaseSync(sourcePath);
|
|
810
|
+
try {
|
|
811
|
+
// Readers that hold an older snapshot keep the WAL as it is: the copy is right either way.
|
|
812
|
+
try {
|
|
813
|
+
source.exec('PRAGMA busy_timeout = 1000');
|
|
814
|
+
source.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
|
|
815
|
+
} catch { /* busy or not a WAL database */ }
|
|
816
|
+
source.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
|
|
817
|
+
if (method === 'clone' || method === 'reflink') {
|
|
818
|
+
let locked = false;
|
|
819
|
+
let cloned = false;
|
|
820
|
+
try {
|
|
821
|
+
source.exec('BEGIN IMMEDIATE');
|
|
822
|
+
locked = true;
|
|
823
|
+
fingerprint = sqliteFingerprint(sourcePath);
|
|
824
|
+
clone(sourcePath, targetPath, method);
|
|
825
|
+
const wal = `${sourcePath}-wal`;
|
|
826
|
+
if (existsSync(wal) && statSync(wal).size > 0) clone(wal, `${targetPath}-wal`, method);
|
|
827
|
+
cloned = true;
|
|
828
|
+
} catch {
|
|
829
|
+
for (const suffix of ['', '-wal']) rmSync(`${targetPath}${suffix}`, { force: true });
|
|
830
|
+
} finally {
|
|
831
|
+
// Nothing was written: the lock is only let go.
|
|
832
|
+
if (locked) { try { source.exec('ROLLBACK'); } catch {} }
|
|
833
|
+
}
|
|
834
|
+
if (!cloned) method = fallback;
|
|
835
|
+
}
|
|
836
|
+
if (method === 'backup') {
|
|
837
|
+
fingerprint = sqliteFingerprint(sourcePath);
|
|
838
|
+
// One step holds one read transaction for the whole copy: no restart, whatever the server writes.
|
|
839
|
+
await nodeSqlite.backup(source, targetPath, { rate: 1_000_000_000 });
|
|
840
|
+
} else if (method === 'vacuum') {
|
|
841
|
+
fingerprint = sqliteFingerprint(sourcePath);
|
|
842
|
+
source.exec(`VACUUM INTO '${escapeSqliteString(targetPath)}'`);
|
|
843
|
+
}
|
|
844
|
+
} finally {
|
|
845
|
+
try { source.close(); } catch {}
|
|
846
|
+
}
|
|
847
|
+
const copy = new DatabaseSync(targetPath);
|
|
848
|
+
try {
|
|
849
|
+
copy.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
850
|
+
copy.exec('PRAGMA journal_mode = DELETE');
|
|
851
|
+
} finally {
|
|
852
|
+
try { copy.close(); } catch {}
|
|
853
|
+
}
|
|
854
|
+
for (const suffix of ['-wal', '-shm']) rmSync(`${targetPath}${suffix}`, { force: true });
|
|
855
|
+
const check = new DatabaseSync(targetPath, { readOnly: true });
|
|
856
|
+
try {
|
|
857
|
+
const pageCount = Number(check.prepare('PRAGMA page_count').get()?.page_count);
|
|
858
|
+
const pageSize = Number(check.prepare('PRAGMA page_size').get()?.page_size);
|
|
859
|
+
const sizeBytes = statSync(targetPath).size;
|
|
860
|
+
if (!(pageCount > 0) || pageCount * pageSize !== sizeBytes) {
|
|
861
|
+
throw new Error(`The copy of ${basename(sourcePath)} is incomplete: ${pageCount} pages of ${pageSize} bytes in a file of ${sizeBytes} bytes`);
|
|
862
|
+
}
|
|
863
|
+
let memoryCount = null;
|
|
864
|
+
if (primary) {
|
|
865
|
+
// A memory database that was never set up has no memories table; any other failure is the copy's.
|
|
866
|
+
const hasMemories = check.prepare("SELECT 1 AS found FROM sqlite_master WHERE type = 'table' AND name = 'memories'").get();
|
|
867
|
+
memoryCount = hasMemories ? Number(check.prepare('SELECT COUNT(*) AS count FROM memories').get().count) : 0;
|
|
868
|
+
}
|
|
869
|
+
return { method, path: targetPath, sizeBytes, memoryCount, pageCount, fingerprint };
|
|
870
|
+
} finally {
|
|
871
|
+
try { check.close(); } catch {}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* SQLite's PRAGMA integrity_check, the one `synabun restore-backup` runs, on each file in `paths`, on a thread of
|
|
877
|
+
* its own (sqlite-integrity-worker.js): a 3.9 GB memory.db takes about 28 s, spent while the archive is written.
|
|
878
|
+
* Resolves [{ path, ok, lines, ms }] (`lines`: SQLite's first findings when a file is not ok); `cancel()` on the
|
|
879
|
+
* promise stops the thread (it then rejects).
|
|
880
|
+
*/
|
|
881
|
+
export function checkSqliteIntegrity(paths, { spawnWorker = (workerData) => new Worker(new URL('./sqlite-integrity-worker.js', import.meta.url), { workerData }) } = {}) {
|
|
882
|
+
if (!paths.length) return Object.assign(Promise.resolve([]), { cancel: () => {} });
|
|
883
|
+
let worker = null;
|
|
884
|
+
const checking = new Promise((resolveCheck, reject) => {
|
|
885
|
+
let settled = false;
|
|
886
|
+
const settle = (fn, value) => { if (!settled) { settled = true; fn(value); } };
|
|
887
|
+
try { worker = spawnWorker({ paths }); }
|
|
888
|
+
catch (error) { reject(error); return; }
|
|
889
|
+
worker.once('message', (results) => settle(resolveCheck, results));
|
|
890
|
+
worker.once('error', (error) => settle(reject, error));
|
|
891
|
+
worker.once('exit', (code) => settle(reject, new Error(`the integrity check ended (exit ${code}) without a result`)));
|
|
892
|
+
});
|
|
893
|
+
checking.cancel = () => { try { worker?.terminate(); } catch { /* already gone */ } };
|
|
894
|
+
return checking;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Raised when a database copy fails SQLite's integrity check: the snapshot is not published. */
|
|
898
|
+
export class SnapshotIntegrityError extends Error {
|
|
899
|
+
constructor(file, lines) {
|
|
900
|
+
super(`The copy of ${file} did not pass SQLite's integrity check: ${lines.slice(0, 3).join('; ') || 'no answer'}`);
|
|
901
|
+
this.name = 'SnapshotIntegrityError';
|
|
902
|
+
this.code = 'EINTEGRITY';
|
|
903
|
+
this.file = file;
|
|
904
|
+
this.lines = lines;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Which file this is, as far as a later reader can tell without reading it: device, inode, size, modified. */
|
|
909
|
+
export function backupFileStamp(path) {
|
|
910
|
+
const stat = statSync(path, { bigint: true });
|
|
911
|
+
return { dev: String(stat.dev), ino: String(stat.ino), size: Number(stat.size), mtimeNs: String(stat.mtimeNs) };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** True when `path` is still the very file `stamp` describes (nothing rewrote or replaced it). */
|
|
915
|
+
export function sameBackupFile(path, stamp) {
|
|
916
|
+
if (!stamp || typeof stamp !== 'object') return false;
|
|
917
|
+
try {
|
|
918
|
+
const now = backupFileStamp(path);
|
|
919
|
+
return now.dev === String(stamp.dev) && now.ino === String(stamp.ino) && now.size === Number(stamp.size) && now.mtimeNs === String(stamp.mtimeNs);
|
|
920
|
+
} catch {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── What an upgrade snapshot protects ──
|
|
926
|
+
|
|
927
|
+
// What lives in data/ and mcp-data/ without being state to roll back, and is rewritten by itself all the time (a
|
|
928
|
+
// snapshot that held it could never stay exact): logs; the start launcher's and the supervisor's records, the
|
|
929
|
+
// update's own handoff, the resume heartbeat of terminal sessions (rewritten every 30 s), the hooks' per-session flags
|
|
930
|
+
// and transcript captures, the MCP tool counters; the caches the server rebuilds from the memories (the map's layout,
|
|
931
|
+
// the links); and the automation browser's Chromium profiles, which Chromium locks and rewrites while it runs (the
|
|
932
|
+
// "attach" one even when SynaBun does not), and which no SynaBun update touches. Upgrade snapshots leave them out;
|
|
933
|
+
// the full backups keep everything.
|
|
934
|
+
const UPGRADE_RUNTIME_FILES = Object.freeze({
|
|
935
|
+
data: new Set(['supervisor.json', 'start-launcher.lock', 'update-handoff.json', 'last-session.json', 'memory-map.json', 'links-cache.json']),
|
|
936
|
+
'mcp-data': new Set(['tool-usage.json']),
|
|
937
|
+
});
|
|
938
|
+
const UPGRADE_RUNTIME_DIRS = Object.freeze({
|
|
939
|
+
data: new Set(['pending-remember', 'pending-compact', 'precompact', 'chrome-profile', 'chrome-attach', 'browser-profiles']),
|
|
940
|
+
'mcp-data': new Set(),
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
/** True for what no upgrade snapshot holds (the list above): `relativePath` inside data/ or mcp-data/ (`dir`). */
|
|
944
|
+
export function isUpgradeRuntimeFile(dir, relativePath) {
|
|
945
|
+
const parts = String(relativePath).replace(/\\/g, '/').split('/');
|
|
946
|
+
if (/\.log(?:\.\d+)?$/i.test(parts.at(-1))) return true;
|
|
947
|
+
if (parts.length === 1) return !!UPGRADE_RUNTIME_FILES[dir]?.has(parts[0]);
|
|
948
|
+
return !!UPGRADE_RUNTIME_DIRS[dir]?.has(parts[0]);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* What an upgrade snapshot protects, as it is now, besides the memory database: `.env` and the state files of data/
|
|
953
|
+
* and mcp-data/ (selectUpgradeSnapshotFiles' choice, without the runtime records above). Each is { archivePath,
|
|
954
|
+
* diskPath, sqlite }: a file that begins with SQLite's header is a database whatever its name, and its journal, WAL
|
|
955
|
+
* and shared-memory files belong to it (never archived on their own).
|
|
956
|
+
*/
|
|
957
|
+
function collectUpgradeInputs({ dataHome, additionalEntries = [], softLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES }) {
|
|
958
|
+
const files = [];
|
|
959
|
+
const envPath = resolve(dataHome, '.env');
|
|
960
|
+
if (existsSync(envPath)) files.push({ diskPath: envPath, archivePath: 'env.bak' });
|
|
961
|
+
const dataDirFilters = {
|
|
962
|
+
data: rel => !shouldSkipDataPath(rel) && !isUpgradeRuntimeFile('data', rel),
|
|
963
|
+
'mcp-data': rel => !shouldSkipDataPath(rel)
|
|
964
|
+
&& !isUpgradeRuntimeFile('mcp-data', rel)
|
|
965
|
+
&& !isDatabaseCopyArtifact(rel)
|
|
966
|
+
&& !/^memory\.db(?:-wal|-shm|-journal)?$/i.test(rel),
|
|
967
|
+
};
|
|
968
|
+
for (const dir of BACKUP_DATA_DIRS) files.push(...collectFiles(resolve(dataHome, dir), dir, { filter: dataDirFilters[dir] }));
|
|
969
|
+
files.push(...normalizeAdditionalEntries(additionalEntries));
|
|
970
|
+
const deduped = [...new Map(files.map(file => [file.archivePath.replace(/\\/g, '/'), file])).values()];
|
|
971
|
+
const { files: kept, excluded } = selectUpgradeSnapshotFiles(deduped, { softLimitBytes });
|
|
972
|
+
const marked = kept.map(file => ({ ...file, sqlite: isSqliteDatabaseFile(file.diskPath) }));
|
|
973
|
+
const databases = new Set(marked.filter(file => file.sqlite).map(file => file.archivePath.toLowerCase()));
|
|
974
|
+
const sideFile = (archivePath) => {
|
|
975
|
+
const lower = archivePath.toLowerCase();
|
|
976
|
+
return SQLITE_SIDE_FILES.some(suffix => suffix && lower.endsWith(suffix) && databases.has(lower.slice(0, -suffix.length)));
|
|
977
|
+
};
|
|
978
|
+
return { inputs: marked.filter(file => !sideFile(file.archivePath)), excluded };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Where an input is, as the manifest records it: relative to the data home (forward slashes), absolute outside it. */
|
|
982
|
+
function inputPath(dataHome, diskPath) {
|
|
983
|
+
const rel = relative(resolve(dataHome), resolve(diskPath));
|
|
984
|
+
return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel.split(sep).join('/') : resolve(diskPath);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Whether the upgrade snapshot whose manifest is `manifest` still stands for the data home as it is now, whichever
|
|
989
|
+
* process wrote since: the same protected files (none new, none gone), every database with the fingerprint it had
|
|
990
|
+
* when it was copied (sqliteFingerprint), every other file with the very content that was archived (its SHA-256 in
|
|
991
|
+
* the manifest). Resolves { fresh, reason, changed: [{ entry, path, why }] }; it stops at the third difference. A
|
|
992
|
+
* snapshot that does not record what it was taken from is never fresh.
|
|
993
|
+
*/
|
|
994
|
+
export async function checkUpgradeSnapshotFreshness({
|
|
995
|
+
dataHome, manifest, databasePath = resolve(dataHome, 'mcp-data', 'memory.db'), upgradeSoftLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES,
|
|
996
|
+
} = {}) {
|
|
997
|
+
const recorded = manifest?.inputs?.entries;
|
|
998
|
+
if (!dataHome || !Array.isArray(recorded)) return { fresh: false, reason: 'it does not record what it was taken from', changed: [] };
|
|
999
|
+
const current = new Map(collectUpgradeInputs({ dataHome, softLimitBytes: upgradeSoftLimitBytes }).inputs.map(file => [file.archivePath, file]));
|
|
1000
|
+
if (existsSync(databasePath)) current.set('database/memory.db', { archivePath: 'database/memory.db', diskPath: databasePath, sqlite: true });
|
|
1001
|
+
const before = new Map(recorded.filter(item => item && typeof item.entry === 'string').map(item => [item.entry, item]));
|
|
1002
|
+
const changed = [];
|
|
1003
|
+
const differs = (entry, path, why) => changed.push({ entry, path, why }) >= 3;
|
|
1004
|
+
scan: {
|
|
1005
|
+
for (const [entry, file] of current) {
|
|
1006
|
+
if (!before.has(entry) && differs(entry, inputPath(dataHome, file.diskPath), 'is new')) break scan;
|
|
1007
|
+
}
|
|
1008
|
+
for (const [entry, item] of before) {
|
|
1009
|
+
const file = current.get(entry);
|
|
1010
|
+
const path = typeof item.path === 'string' && item.path ? item.path : entry;
|
|
1011
|
+
if (!file) { if (differs(entry, path, 'is gone')) break scan; continue; }
|
|
1012
|
+
if (!!item.sqlite !== !!file.sqlite) { if (differs(entry, path, 'changed')) break scan; continue; }
|
|
1013
|
+
if (item.sqlite) {
|
|
1014
|
+
let now = null;
|
|
1015
|
+
try { now = sqliteFingerprint(file.diskPath); } catch { /* gone or unreadable: written */ }
|
|
1016
|
+
if (!sameSqliteFingerprint(item.sqlite, now) && differs(entry, path, 'was written')) break scan;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
let actual = null;
|
|
1020
|
+
try { actual = `sha256:${await hashFile(file.diskPath)}`; } catch { /* gone or unreadable: changed */ }
|
|
1021
|
+
if (actual !== manifest.checksums?.[entry] && differs(entry, path, 'changed')) break scan;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return changed.length
|
|
1025
|
+
? { fresh: false, reason: changed.map(item => `${item.path} ${item.why}`).join(', '), changed }
|
|
1026
|
+
: { fresh: true, reason: null, changed };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The snapshot SynaBun takes before a new version first runs on the data (kinds pre-update and pre-migration),
|
|
1031
|
+
* built for the moment it is taken: a restart into an update, with the user waiting.
|
|
1032
|
+
*
|
|
1033
|
+
* what collectUpgradeInputs: .env, the memory database and the state files of data/ and mcp-data/
|
|
1034
|
+
* (generated media, logs and runtime records, the copies of memory.db that migrations leave, and the
|
|
1035
|
+
* backups folder are never in it)
|
|
1036
|
+
* databases memory.db and every other SQLite file, known by its header whatever its name, each as of one commit
|
|
1037
|
+
* (copySqliteDatabase: a clone under the write lock where the file system clones, SQLite's online
|
|
1038
|
+
* backup elsewhere). A database that cannot be copied that way fails the snapshot: it is never
|
|
1039
|
+
* archived as a plain file, which could lose committed rows or mix two moments
|
|
1040
|
+
* integrity every database copy passes PRAGMA integrity_check (checkSqliteIntegrity, on a thread of its own while
|
|
1041
|
+
* the archive is written and read back), the check `synabun restore-backup` runs
|
|
1042
|
+
* archive one ZIP (backup-zip-writer.js): databases stored as they are, small files deflated, each entry's
|
|
1043
|
+
* CRC-32 and SHA-256 computed as it is written; the manifest lists every SHA-256 and, in `inputs`,
|
|
1044
|
+
* what each entry was taken from (a database's fingerprint) for checkUpgradeSnapshotFreshness
|
|
1045
|
+
* verified the finished ZIP is read back once (every entry's CRC-32, size and SHA-256 against the manifest);
|
|
1046
|
+
* it is published, with a rename, only when that and the integrity check have passed
|
|
1047
|
+
*
|
|
1048
|
+
* The same archive layout and manifest as createVerifiedBackup(), so Settings > restore and `synabun
|
|
1049
|
+
* restore-backup` restore it as they restore any backup.
|
|
1050
|
+
*
|
|
1051
|
+
* onProgress({ phase: 'collect' | 'copy' | 'archive' | 'verify' | 'integrity', ... }): collect says how much (files,
|
|
1052
|
+
* bytes, skipped), archive and verify say how far (done, total, in bytes); integrity says that the archive is ready
|
|
1053
|
+
* and the integrity check still runs (state 'running'), then that it passed (state 'ok').
|
|
1054
|
+
*/
|
|
1055
|
+
export async function createUpgradeSnapshot({
|
|
1056
|
+
dataHome,
|
|
1057
|
+
databasePath = resolve(dataHome, 'mcp-data', 'memory.db'),
|
|
1058
|
+
folderPath,
|
|
1059
|
+
kind = 'pre-update',
|
|
1060
|
+
appVersion = null,
|
|
1061
|
+
dataSchemaVersion = 1,
|
|
1062
|
+
sourceFingerprint = null,
|
|
1063
|
+
additionalEntries = [],
|
|
1064
|
+
freeSpaceReserveBytes = BACKUP_FREE_SPACE_RESERVE_BYTES,
|
|
1065
|
+
getAvailableBytes = backupAvailableBytes,
|
|
1066
|
+
onProgress = null,
|
|
1067
|
+
upgradeSoftLimitBytes = UPGRADE_SNAPSHOT_SOFT_LIMIT_BYTES,
|
|
1068
|
+
copyDatabase = copySqliteDatabase,
|
|
1069
|
+
checkIntegrity = checkSqliteIntegrity,
|
|
1070
|
+
now = () => new Date(),
|
|
1071
|
+
} = {}) {
|
|
1072
|
+
const report = (detail) => { try { if (typeof onProgress === 'function') onProgress(detail); } catch { /* reporting never stops a snapshot */ } };
|
|
1073
|
+
if (!dataHome || !folderPath) throw new Error('dataHome and folderPath are required');
|
|
1074
|
+
if (!UPGRADE_BACKUP_KINDS.has(kind)) throw new Error(`An upgrade snapshot is ${[...UPGRADE_BACKUP_KINDS].join(' or ')}, not ${kind}`);
|
|
1075
|
+
const started = Date.now();
|
|
1076
|
+
const createdAt = now();
|
|
1077
|
+
assertBackupDestinationMounted(folderPath);
|
|
1078
|
+
mkdirSync(folderPath, { recursive: true });
|
|
1079
|
+
cleanupStaleBackupArtifacts(folderPath);
|
|
1080
|
+
const destination = resolve(folderPath, archiveNameFor(kind, createdAt));
|
|
1081
|
+
if (existsSync(destination)) throw new Error(`Backup already exists: ${destination}`);
|
|
1082
|
+
const tempPath = `${destination}.${process.pid}.tmp`;
|
|
1083
|
+
// Everything this snapshot copies lives here until the archive is published (an abandoned one is swept after an hour).
|
|
1084
|
+
const work = mkdtempSync(resolve(folderPath, `.synabun-files-${process.pid}-`));
|
|
1085
|
+
const timings = {};
|
|
1086
|
+
const mark = (name, since) => { timings[name] = Date.now() - since; };
|
|
1087
|
+
// The integrity check of the database copies, once they exist: { results, ms } or { error, ms }, never a rejection.
|
|
1088
|
+
let integrity = null;
|
|
1089
|
+
try {
|
|
1090
|
+
let since = Date.now();
|
|
1091
|
+
const { inputs: kept, excluded } = collectUpgradeInputs({ dataHome, additionalEntries, softLimitBytes: upgradeSoftLimitBytes });
|
|
1092
|
+
const databaseBytes = existsSync(databasePath) ? statSync(databasePath).size : 0;
|
|
1093
|
+
const walBytes = existsSync(`${databasePath}-wal`) ? statSync(`${databasePath}-wal`).size : 0;
|
|
1094
|
+
const keptBytes = kept.reduce((sum, file) => sum + backupFileSize(file), 0);
|
|
1095
|
+
report({
|
|
1096
|
+
phase: 'collect',
|
|
1097
|
+
files: kept.length + (databaseBytes ? 1 : 0),
|
|
1098
|
+
bytes: keptBytes + databaseBytes,
|
|
1099
|
+
skippedFiles: excluded?.files ?? 0,
|
|
1100
|
+
skippedBytes: excluded?.bytes ?? 0,
|
|
1101
|
+
skippedReason: excluded?.reason ?? null,
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// Room for it: the archive holds everything once; a database that cannot be cloned is copied first.
|
|
1105
|
+
const cloned = !!cloneMethodFor(databasePath, work);
|
|
1106
|
+
const sqliteBytes = kept.filter(file => file.sqlite).reduce((sum, file) => sum + backupFileSize(file), 0);
|
|
1107
|
+
const stagedBytes = kept.filter(file => !file.sqlite && !IMMUTABLE_BACKUP_EXTENSIONS.has(extname(file.archivePath).toLowerCase()))
|
|
1108
|
+
.reduce((sum, file) => sum + backupFileSize(file), 0);
|
|
1109
|
+
const requiredBytes = Math.max(0, Number(freeSpaceReserveBytes) || 0)
|
|
1110
|
+
+ keptBytes + databaseBytes + walBytes
|
|
1111
|
+
+ (cloned ? walBytes : databaseBytes + walBytes) + sqliteBytes + stagedBytes;
|
|
1112
|
+
const availableBytes = Number(getAvailableBytes(folderPath));
|
|
1113
|
+
if (Number.isFinite(availableBytes) && availableBytes < requiredBytes) throw new BackupStorageError({ availableBytes, requiredBytes, folderPath });
|
|
1114
|
+
mark('collect', since);
|
|
1115
|
+
|
|
1116
|
+
since = Date.now();
|
|
1117
|
+
report({ phase: 'copy', bytes: databaseBytes, method: cloned ? 'clone' : 'copy' });
|
|
1118
|
+
const entries = [];
|
|
1119
|
+
// What each entry was taken from: checkUpgradeSnapshotFreshness compares the data home with it later.
|
|
1120
|
+
const recorded = [];
|
|
1121
|
+
const copies = [];
|
|
1122
|
+
const database = await copyDatabase(databasePath, resolve(work, 'memory.db'), { primary: true });
|
|
1123
|
+
if (database) {
|
|
1124
|
+
entries.push({ diskPath: database.path, archivePath: 'database/memory.db', store: true });
|
|
1125
|
+
recorded.push({ entry: 'database/memory.db', path: inputPath(dataHome, databasePath), sqlite: database.fingerprint });
|
|
1126
|
+
copies.push({ entry: 'database/memory.db', path: database.path });
|
|
1127
|
+
}
|
|
1128
|
+
for (const [index, file] of kept.entries()) {
|
|
1129
|
+
if (file.sqlite) {
|
|
1130
|
+
let copied;
|
|
1131
|
+
try { copied = await copyDatabase(file.diskPath, resolve(work, `db-${index}.db`)); }
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
throw Object.assign(new Error(`${file.archivePath} could not be copied as of one commit: ${error?.message || error}`), { cause: error, code: error?.code });
|
|
1134
|
+
}
|
|
1135
|
+
if (!copied) continue; // gone since it was listed
|
|
1136
|
+
entries.push({ diskPath: copied.path, archivePath: file.archivePath, store: true });
|
|
1137
|
+
recorded.push({ entry: file.archivePath, path: inputPath(dataHome, file.diskPath), sqlite: copied.fingerprint });
|
|
1138
|
+
copies.push({ entry: file.archivePath, path: copied.path });
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const store = INCOMPRESSIBLE_EXTENSIONS.has(extname(file.archivePath).toLowerCase());
|
|
1142
|
+
if (IMMUTABLE_BACKUP_EXTENSIONS.has(extname(file.archivePath).toLowerCase())) {
|
|
1143
|
+
entries.push({ diskPath: file.diskPath, archivePath: file.archivePath, store });
|
|
1144
|
+
recorded.push({ entry: file.archivePath, path: inputPath(dataHome, file.diskPath) });
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
// A moment's copy, so the bytes archived and the bytes checksummed are the same bytes.
|
|
1148
|
+
const staged = resolve(work, `file-${index}`);
|
|
1149
|
+
try { copyFileSync(file.diskPath, staged); }
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
if (error?.code === 'ENOENT') continue;
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
entries.push({ diskPath: staged, archivePath: file.archivePath, store });
|
|
1155
|
+
recorded.push({ entry: file.archivePath, path: inputPath(dataHome, file.diskPath) });
|
|
1156
|
+
}
|
|
1157
|
+
entries.sort((a, b) => a.archivePath.localeCompare(b.archivePath));
|
|
1158
|
+
recorded.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
1159
|
+
mark('copy', since);
|
|
1160
|
+
|
|
1161
|
+
// From now on, beside the archive: SQLite's integrity check of every copy, as the restore runs it.
|
|
1162
|
+
const integrityStarted = Date.now();
|
|
1163
|
+
let integrityDone = false;
|
|
1164
|
+
const checking = checkIntegrity(copies.map(copy => copy.path));
|
|
1165
|
+
integrity = Promise.resolve(checking).then(
|
|
1166
|
+
(results) => ({ results, ms: Date.now() - integrityStarted }),
|
|
1167
|
+
(error) => ({ error, ms: Date.now() - integrityStarted }),
|
|
1168
|
+
).finally(() => { integrityDone = true; });
|
|
1169
|
+
integrity.cancel = () => { try { checking?.cancel?.(); } catch {} };
|
|
1170
|
+
|
|
1171
|
+
since = Date.now();
|
|
1172
|
+
const total = entries.reduce((sum, entry) => sum + statSync(entry.diskPath).size, 0);
|
|
1173
|
+
report({ phase: 'archive', done: 0, total });
|
|
1174
|
+
const manifestFor = (written) => {
|
|
1175
|
+
const checksums = {};
|
|
1176
|
+
for (const item of written) checksums[item.name.slice(BACKUP_PREFIX.length + 1)] = `sha256:${item.sha256}`;
|
|
1177
|
+
return JSON.stringify({
|
|
1178
|
+
version: BACKUP_MANIFEST_VERSION,
|
|
1179
|
+
created: createdAt.toISOString(),
|
|
1180
|
+
kind,
|
|
1181
|
+
appVersion,
|
|
1182
|
+
dataSchemaVersion,
|
|
1183
|
+
sourceFingerprint,
|
|
1184
|
+
storage: 'sqlite',
|
|
1185
|
+
database: database ? {
|
|
1186
|
+
file: 'database/memory.db',
|
|
1187
|
+
sizeBytes: database.sizeBytes,
|
|
1188
|
+
memoryCount: database.memoryCount,
|
|
1189
|
+
// The archive is published only once this check has passed on every database copy in it.
|
|
1190
|
+
integrity: 'ok',
|
|
1191
|
+
copy: database.method,
|
|
1192
|
+
} : null,
|
|
1193
|
+
files: entries.map(entry => entry.archivePath),
|
|
1194
|
+
checksums,
|
|
1195
|
+
scope: 'state-only',
|
|
1196
|
+
excluded,
|
|
1197
|
+
inputs: { version: 1, entries: recorded },
|
|
1198
|
+
verified: true,
|
|
1199
|
+
verification: { algorithm: 'sha256', publication: 'verify-before-atomic-rename', writer: 'one-pass', integrity: 'integrity_check' },
|
|
1200
|
+
}, null, 2);
|
|
1201
|
+
};
|
|
1202
|
+
await writeZipArchive(tempPath, [
|
|
1203
|
+
...entries.map(entry => ({ name: `${BACKUP_PREFIX}/${entry.archivePath}`, diskPath: entry.diskPath, store: entry.store })),
|
|
1204
|
+
{ name: `${BACKUP_PREFIX}/manifest.json`, data: manifestFor },
|
|
1205
|
+
], { now, onProgress: ({ bytes, totalBytes }) => report({ phase: 'archive', done: bytes, total: Math.max(total, totalBytes) }) });
|
|
1206
|
+
mark('archive', since);
|
|
1207
|
+
|
|
1208
|
+
since = Date.now();
|
|
1209
|
+
let checked = 0;
|
|
1210
|
+
let reportedAt = 0;
|
|
1211
|
+
report({ phase: 'verify', done: 0, total });
|
|
1212
|
+
const publishedManifest = await verifyBackupArchive(tempPath, {
|
|
1213
|
+
onBytes: (bytes) => {
|
|
1214
|
+
checked += bytes;
|
|
1215
|
+
if (checked - reportedAt >= 64 * 1024 * 1024 || checked >= total) { reportedAt = checked; report({ phase: 'verify', done: checked, total }); }
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
mark('verify', since);
|
|
1219
|
+
|
|
1220
|
+
since = Date.now();
|
|
1221
|
+
if (!integrityDone) report({ phase: 'integrity', state: 'running' });
|
|
1222
|
+
const verdict = await integrity;
|
|
1223
|
+
timings.integrity = verdict.ms;
|
|
1224
|
+
mark('integrityWait', since);
|
|
1225
|
+
if (verdict.error) {
|
|
1226
|
+
throw Object.assign(new Error(`SQLite's integrity check of the copies could not run: ${verdict.error?.message || verdict.error}`), { code: 'EINTEGRITY' });
|
|
1227
|
+
}
|
|
1228
|
+
for (const [index, result] of copies.entries()) {
|
|
1229
|
+
const found = (verdict.results || []).find(item => item?.path === result.path) || verdict.results?.[index];
|
|
1230
|
+
if (!found?.ok) throw new SnapshotIntegrityError(result.entry, found?.lines || []);
|
|
1231
|
+
}
|
|
1232
|
+
report({ phase: 'integrity', state: 'ok' });
|
|
1233
|
+
// The copies are in the archive and checked: their room goes back.
|
|
1234
|
+
try { rmSync(work, { recursive: true, force: true }); } catch {}
|
|
1235
|
+
renameSync(tempPath, destination);
|
|
1236
|
+
return {
|
|
1237
|
+
path: destination,
|
|
1238
|
+
name: basename(destination),
|
|
1239
|
+
kind,
|
|
1240
|
+
createdAt: createdAt.toISOString(),
|
|
1241
|
+
sizeBytes: statSync(destination).size,
|
|
1242
|
+
manifest: publishedManifest,
|
|
1243
|
+
stamp: backupFileStamp(destination),
|
|
1244
|
+
database: database ? { method: database.method, sizeBytes: database.sizeBytes, memoryCount: database.memoryCount, integrity: 'ok' } : null,
|
|
1245
|
+
timings: { ...timings, total: Date.now() - started },
|
|
1246
|
+
};
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
try { if (existsSync(tempPath)) unlinkSync(tempPath); } catch {}
|
|
1249
|
+
throw error;
|
|
1250
|
+
} finally {
|
|
1251
|
+
// A check still running reads the copies: it ends first (its thread is stopped), then they go.
|
|
1252
|
+
if (integrity) {
|
|
1253
|
+
integrity.cancel();
|
|
1254
|
+
await integrity;
|
|
1255
|
+
}
|
|
1256
|
+
try { rmSync(work, { recursive: true, force: true }); } catch {}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
638
1260
|
function dateFromSnapshotName(name, stat) {
|
|
639
1261
|
const match = name.match(/^synabun-[a-z-]+-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)\.zip$/i);
|
|
640
1262
|
if (!match) return stat.mtime;
|
|
@@ -705,7 +1327,7 @@ export function applyBackupRetention({
|
|
|
705
1327
|
keep.add(latestPreUpdate.path);
|
|
706
1328
|
protectedPaths.add(latestPreUpdate.path);
|
|
707
1329
|
}
|
|
708
|
-
for (const item of snapshots.filter(item => ['pre-update', 'pre-migration'].includes(item.kind)).slice(0,
|
|
1330
|
+
for (const item of snapshots.filter(item => ['pre-update', 'pre-migration'].includes(item.kind)).slice(0, Math.max(0, Number(policy.preUpdateKeep) || 0))) keep.add(item.path);
|
|
709
1331
|
|
|
710
1332
|
const daily = new Set();
|
|
711
1333
|
const weekly = new Set();
|