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