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.
Files changed (656) hide show
  1. package/CHANGELOG.md +207 -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 +13 -13
  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 +30 -17
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +7 -6
  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 +302 -0
  24. package/lib/virustotal.mjs +203 -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/budget-judgments.d.ts +1 -1
  37. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  38. package/mcp-server/dist/services/budget-judgments.js +5 -3
  39. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  40. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  41. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  42. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  43. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +2 -2
  45. package/mcp-server/dist/services/identity.js +2 -2
  46. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  47. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  48. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  49. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  50. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  51. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  52. package/mcp-server/dist/services/memory-sessions.js +28 -2
  53. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  54. package/mcp-server/dist/services/neural-interface.d.ts +18 -11
  55. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  56. package/mcp-server/dist/services/neural-interface.js +67 -11
  57. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  58. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  59. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  60. package/mcp-server/dist/services/server-instructions.js +22 -63
  61. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  62. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  63. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  64. package/mcp-server/dist/services/tool-catalog.js +339 -0
  65. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  66. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  67. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  68. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  69. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  70. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  71. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  72. package/mcp-server/dist/services/tool-list.js +334 -0
  73. package/mcp-server/dist/services/tool-list.js.map +1 -0
  74. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  75. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  76. package/mcp-server/dist/services/tool-runtime.js +400 -0
  77. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  78. package/mcp-server/dist/services/typesafe-config.js +1 -1
  79. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  80. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  81. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  82. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  83. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  84. package/mcp-server/dist/tools/agents.d.ts +14 -14
  85. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  86. package/mcp-server/dist/tools/agents.js +29 -19
  87. package/mcp-server/dist/tools/agents.js.map +1 -1
  88. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  89. package/mcp-server/dist/tools/choice.d.ts +6 -0
  90. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  91. package/mcp-server/dist/tools/choice.js +18 -13
  92. package/mcp-server/dist/tools/choice.js.map +1 -1
  93. package/mcp-server/dist/tools/git-tools.js +2 -2
  94. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  95. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  96. package/mcp-server/dist/tools/image-tools.js +1 -1
  97. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  98. package/mcp-server/dist/tools/morelogin.js +9 -6
  99. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  100. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  101. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  102. package/mcp-server/dist/tools/skill-tools.js +642 -0
  103. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  104. package/mcp-server/dist/tools/skill.d.ts +7 -0
  105. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  106. package/mcp-server/dist/tools/skill.js +11 -0
  107. package/mcp-server/dist/tools/skill.js.map +1 -0
  108. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  109. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  110. package/mcp-server/dist/tools/tool-groups.js +147 -0
  111. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  112. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  113. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  114. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  115. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  116. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  117. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  118. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  119. package/mcp-server/package-lock.json +2 -2
  120. package/mcp-server/package.json +1 -1
  121. package/mcp-server/src/http.ts +75 -12
  122. package/mcp-server/src/index.ts +147 -154
  123. package/mcp-server/src/preload.ts +12 -12
  124. package/mcp-server/src/services/budget-judgments.ts +5 -3
  125. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  126. package/mcp-server/src/services/identity.ts +2 -2
  127. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  128. package/mcp-server/src/services/memory-sessions.ts +24 -3
  129. package/mcp-server/src/services/neural-interface.ts +81 -21
  130. package/mcp-server/src/services/server-instructions.ts +29 -64
  131. package/mcp-server/src/services/tool-catalog.ts +403 -0
  132. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  133. package/mcp-server/src/services/tool-list.ts +410 -0
  134. package/mcp-server/src/services/tool-runtime.ts +457 -0
  135. package/mcp-server/src/services/typesafe-config.ts +1 -1
  136. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  137. package/mcp-server/src/tools/agents.ts +29 -20
  138. package/mcp-server/src/tools/choice.ts +17 -12
  139. package/mcp-server/src/tools/git-tools.ts +2 -2
  140. package/mcp-server/src/tools/image-tools.ts +1 -1
  141. package/mcp-server/src/tools/morelogin.ts +6 -4
  142. package/mcp-server/src/tools/skill-tools.ts +592 -0
  143. package/mcp-server/src/tools/skill.ts +13 -0
  144. package/mcp-server/src/tools/tool-groups.ts +164 -0
  145. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  146. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  147. package/neural-interface/i18n/en.json +3953 -228
  148. package/neural-interface/i18n/pt-BR.json +3901 -179
  149. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  150. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  151. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  152. package/neural-interface/lib/antigravity/bypass.js +100 -0
  153. package/neural-interface/lib/antigravity/catalog.js +273 -0
  154. package/neural-interface/lib/antigravity/home.js +260 -0
  155. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  156. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  157. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  158. package/neural-interface/lib/antigravity/policy.js +74 -0
  159. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  160. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  161. package/neural-interface/lib/antigravity/usage.js +43 -0
  162. package/neural-interface/lib/assistant-api.js +128 -28
  163. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  164. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  165. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  166. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  167. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  168. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  169. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  170. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  171. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  172. package/neural-interface/lib/assistant-budget-split.js +42 -15
  173. package/neural-interface/lib/assistant-catalog.js +81 -15
  174. package/neural-interface/lib/assistant-config.js +6 -3
  175. package/neural-interface/lib/assistant-dispatch.js +1073 -200
  176. package/neural-interface/lib/assistant-envelope.js +156 -2
  177. package/neural-interface/lib/assistant-evidence.js +80 -0
  178. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  179. package/neural-interface/lib/assistant-persona.js +93 -19
  180. package/neural-interface/lib/assistant-plan-permissions.js +25 -9
  181. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  182. package/neural-interface/lib/assistant-route-gate.js +6 -4
  183. package/neural-interface/lib/assistant-router.js +245 -38
  184. package/neural-interface/lib/assistant-runtime.js +214 -30
  185. package/neural-interface/lib/assistant-task-prompt.js +87 -21
  186. package/neural-interface/lib/assistant-tools.js +51 -0
  187. package/neural-interface/lib/assistant-usage.js +29 -0
  188. package/neural-interface/lib/backup-service.js +626 -4
  189. package/neural-interface/lib/backup-zip-reader.js +10 -5
  190. package/neural-interface/lib/backup-zip-writer.js +257 -0
  191. package/neural-interface/lib/browser-execution.js +10 -2
  192. package/neural-interface/lib/browser-guard-cdp.js +84 -0
  193. package/neural-interface/lib/browser-network-guard.js +476 -0
  194. package/neural-interface/lib/browser-policy-roots.js +64 -0
  195. package/neural-interface/lib/browser-tool-policy.js +198 -38
  196. package/neural-interface/lib/claude-agent-bridge.js +11 -2
  197. package/neural-interface/lib/claude-history-cache.js +210 -0
  198. package/neural-interface/lib/claude-hooks-status.js +103 -0
  199. package/neural-interface/lib/claude-model-catalog.js +5 -1
  200. package/neural-interface/lib/claude-temporary.js +5 -11
  201. package/neural-interface/lib/cli-install-api.js +1 -1
  202. package/neural-interface/lib/cli-install.js +163 -154
  203. package/neural-interface/lib/cli-resolver.js +13 -3
  204. package/neural-interface/lib/cli-update-planner.js +52 -41
  205. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  206. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  207. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  208. package/neural-interface/lib/codex-worker-app-server.js +191 -0
  209. package/neural-interface/lib/confined-fs.js +30 -3
  210. package/neural-interface/lib/crash-notice-api.js +98 -0
  211. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  212. package/neural-interface/lib/editor-git-api.js +3 -20
  213. package/neural-interface/lib/exec-loop-command.js +13 -10
  214. package/neural-interface/lib/file-explorer-api.js +74 -40
  215. package/neural-interface/lib/file-explorer-watch.js +9 -3
  216. package/neural-interface/lib/git-bash/install.js +52 -6
  217. package/neural-interface/lib/git-bash/manager.js +59 -17
  218. package/neural-interface/lib/git-bash/pins.js +12 -0
  219. package/neural-interface/lib/git-read.js +2 -2
  220. package/neural-interface/lib/git-status-runner.js +390 -0
  221. package/neural-interface/lib/git-status-watchdog.js +40 -0
  222. package/neural-interface/lib/greeting/budgets.js +1 -0
  223. package/neural-interface/lib/greeting/engine.js +2 -2
  224. package/neural-interface/lib/greeting/sources.js +10 -12
  225. package/neural-interface/lib/greeting/writer.js +1 -1
  226. package/neural-interface/lib/http-guards.js +8 -2
  227. package/neural-interface/lib/install-details.js +4 -1
  228. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  229. package/neural-interface/lib/mcp-client-config.js +35 -33
  230. package/neural-interface/lib/mcp-external-servers.js +332 -0
  231. package/neural-interface/lib/mcp-installer.js +2 -1
  232. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  233. package/neural-interface/lib/memory-session-capture.js +17 -3
  234. package/neural-interface/lib/morelogin.js +177 -25
  235. package/neural-interface/lib/native-loop-providers.js +965 -66
  236. package/neural-interface/lib/native-loop-runtime.js +290 -42
  237. package/neural-interface/lib/opencode-config.js +557 -0
  238. package/neural-interface/lib/opencode-db-schema.js +17 -34
  239. package/neural-interface/lib/opencode-history.js +48 -218
  240. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  241. package/neural-interface/lib/opencode-native-boundary.js +53 -0
  242. package/neural-interface/lib/opencode-protocol.js +249 -112
  243. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  244. package/neural-interface/lib/opencode-serve-records.js +39 -21
  245. package/neural-interface/lib/opencode-v2-client.js +48 -153
  246. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  247. package/neural-interface/lib/opencode-worker-data.js +27 -0
  248. package/neural-interface/lib/opencode2-client.js +73 -37
  249. package/neural-interface/lib/opencode2-proxy.js +23 -63
  250. package/neural-interface/lib/opencode2-translate.js +51 -2
  251. package/neural-interface/lib/owner-authority.js +249 -0
  252. package/neural-interface/lib/page-session.js +275 -0
  253. package/neural-interface/lib/path-confine.js +12 -9
  254. package/neural-interface/lib/peer-identity.js +793 -0
  255. package/neural-interface/lib/pty-host/core.js +28 -1
  256. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  257. package/neural-interface/lib/pty-host/manager.js +12 -1
  258. package/neural-interface/lib/remote-policy.js +1 -1
  259. package/neural-interface/lib/request-gate.js +20 -2
  260. package/neural-interface/lib/rulesets/api.js +7 -6
  261. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  262. package/neural-interface/lib/rulesets/installer.js +180 -36
  263. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  264. package/neural-interface/lib/rulesets/render.js +1 -1
  265. package/neural-interface/lib/session-cache-builder.js +81 -31
  266. package/neural-interface/lib/session-title-generator.js +58 -36
  267. package/neural-interface/lib/skills/api-v2.js +495 -0
  268. package/neural-interface/lib/skills/bundle.js +418 -0
  269. package/neural-interface/lib/skills/catalog.js +298 -0
  270. package/neural-interface/lib/skills/creation-api.js +191 -0
  271. package/neural-interface/lib/skills/creation-brief.js +247 -0
  272. package/neural-interface/lib/skills/creation-sources.js +285 -0
  273. package/neural-interface/lib/skills/creation.js +843 -0
  274. package/neural-interface/lib/skills/deploy.js +569 -0
  275. package/neural-interface/lib/skills/digest.js +119 -0
  276. package/neural-interface/lib/skills/discover.js +111 -0
  277. package/neural-interface/lib/skills/draft-confinement.js +216 -0
  278. package/neural-interface/lib/skills/eval-confinement.js +399 -0
  279. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  280. package/neural-interface/lib/skills/eval-trace.js +135 -0
  281. package/neural-interface/lib/skills/evals-api.js +212 -0
  282. package/neural-interface/lib/skills/evals.js +1573 -0
  283. package/neural-interface/lib/skills/export.js +203 -0
  284. package/neural-interface/lib/skills/frontmatter.js +64 -0
  285. package/neural-interface/lib/skills/health.js +75 -0
  286. package/neural-interface/lib/skills/json-edit.js +165 -0
  287. package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
  288. package/neural-interface/lib/skills/known-bridges.js +96 -0
  289. package/neural-interface/lib/skills/library.js +823 -0
  290. package/neural-interface/lib/skills/local-store.js +21 -0
  291. package/neural-interface/lib/skills/migration.js +433 -0
  292. package/neural-interface/lib/skills/operations.js +45 -0
  293. package/neural-interface/lib/skills/ownership.js +427 -0
  294. package/neural-interface/lib/skills/packages.js +88 -0
  295. package/neural-interface/lib/skills/reviews.js +212 -0
  296. package/neural-interface/lib/skills/revision.js +80 -0
  297. package/neural-interface/lib/skills/roots.js +269 -0
  298. package/neural-interface/lib/skills/runtime-checks.js +358 -0
  299. package/neural-interface/lib/skills/scan-assets.js +252 -0
  300. package/neural-interface/lib/skills/scan-context.js +26 -0
  301. package/neural-interface/lib/skills/scan-shell.js +324 -0
  302. package/neural-interface/lib/skills/scan.js +364 -0
  303. package/neural-interface/lib/skills/scanners.js +54 -0
  304. package/neural-interface/lib/skills/service.js +530 -0
  305. package/neural-interface/lib/skills/telemetry.js +119 -0
  306. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  307. package/neural-interface/lib/synabun-version.js +1 -1
  308. package/neural-interface/lib/system-activity.js +1 -1
  309. package/neural-interface/lib/tool-lists.js +124 -0
  310. package/neural-interface/lib/whatsapp/brain.js +7 -3
  311. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  312. package/neural-interface/lib/whatsapp/cards.js +22 -9
  313. package/neural-interface/lib/whatsapp/config.js +1 -1
  314. package/neural-interface/lib/worker-confinement.js +1268 -0
  315. package/neural-interface/lib/worker-guard-setting.js +56 -0
  316. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  317. package/neural-interface/lib/worker-question.js +74 -0
  318. package/neural-interface/lib/worker-relay-ports.js +227 -0
  319. package/neural-interface/lib/worker-resume.js +238 -0
  320. package/neural-interface/lib/worker-sandbox.js +329 -0
  321. package/neural-interface/package-lock.json +34 -12
  322. package/neural-interface/package.json +3 -2
  323. package/neural-interface/public/index.html +3 -1
  324. package/neural-interface/public/index2d.html +19 -11
  325. package/neural-interface/public/onboarding.html +3 -3
  326. package/neural-interface/public/shared/account-menu.js +8 -5
  327. package/neural-interface/public/shared/agent-runtime-options.js +125 -31
  328. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  329. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  330. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  331. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  332. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  333. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  334. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  335. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  336. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  337. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  338. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  339. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  340. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  341. package/neural-interface/public/shared/api.js +1527 -1336
  342. package/neural-interface/public/shared/app-frame.js +192 -0
  343. package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
  344. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  345. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  346. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  347. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  348. package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
  349. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  350. package/neural-interface/public/shared/assistant/asst-control.js +205 -26
  351. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  352. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  353. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  354. package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
  355. package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
  356. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  357. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  358. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  359. package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
  360. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  361. package/neural-interface/public/shared/assistant/asst-render.js +445 -82
  362. package/neural-interface/public/shared/assistant/asst-route.js +304 -53
  363. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  364. package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
  365. package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
  366. package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
  367. package/neural-interface/public/shared/assistant/asst-state.js +87 -24
  368. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  369. package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
  370. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
  371. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  372. package/neural-interface/public/shared/automation-context.js +5 -2
  373. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  374. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  375. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  376. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  377. package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
  378. package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
  379. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  380. package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
  381. package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
  382. package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
  383. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  384. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  385. package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
  386. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  387. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  388. package/neural-interface/public/shared/cli-install-view.js +11 -12
  389. package/neural-interface/public/shared/cli-status.js +11 -10
  390. package/neural-interface/public/shared/constants.js +5 -4
  391. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  392. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  393. package/neural-interface/public/shared/context-menu-model.js +367 -0
  394. package/neural-interface/public/shared/context-menu.js +372 -0
  395. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  396. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  397. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  398. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  399. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  400. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  401. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  402. package/neural-interface/public/shared/desktop-update-view.js +21 -0
  403. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  404. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  405. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  406. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  407. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  408. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  409. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  410. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  411. package/neural-interface/public/shared/html-shell.js +30 -15
  412. package/neural-interface/public/shared/i18n.js +2 -0
  413. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  414. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  415. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  416. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  417. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  418. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  419. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  420. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  421. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  422. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  423. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  424. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
  425. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  426. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  427. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  428. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
  429. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  430. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  431. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  432. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  433. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  434. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  435. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  436. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
  437. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  438. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
  439. package/neural-interface/public/shared/page-session.js +96 -0
  440. package/neural-interface/public/shared/provider-icons.js +4 -2
  441. package/neural-interface/public/shared/registry.js +5 -3
  442. package/neural-interface/public/shared/retired-2d.js +3 -0
  443. package/neural-interface/public/shared/schedules-view.js +195 -61
  444. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  445. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  446. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  447. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  448. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  449. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  450. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  451. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  452. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  453. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  454. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  455. package/neural-interface/public/shared/skills/ss-creation.js +222 -0
  456. package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
  457. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  458. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  459. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  460. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  461. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  462. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  463. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  464. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  465. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  466. package/neural-interface/public/shared/skills/ss-lint.js +149 -0
  467. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  468. package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
  469. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  470. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  471. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  472. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  473. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  474. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  475. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  476. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
  477. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  478. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  479. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  480. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  481. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  482. package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
  483. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  484. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  485. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  486. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  487. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  488. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  489. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  490. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  491. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
  492. package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
  493. package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
  494. package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
  495. package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
  496. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  497. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  498. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  499. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  500. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  501. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  502. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  503. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  504. package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
  505. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  506. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  507. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  508. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
  509. package/neural-interface/public/shared/state.js +1 -1
  510. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  511. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  512. package/neural-interface/public/shared/styles.css +1822 -1756
  513. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  514. package/neural-interface/public/shared/text-cache.js +29 -0
  515. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  516. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  517. package/neural-interface/public/shared/tool-list-view.js +112 -0
  518. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  519. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  520. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  521. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  522. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  523. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  524. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  525. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  526. package/neural-interface/public/shared/tour/tour.css +144 -0
  527. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  528. package/neural-interface/public/shared/ui-assistant.js +2 -0
  529. package/neural-interface/public/shared/ui-automation-studio.js +111 -140
  530. package/neural-interface/public/shared/ui-claude-panel.js +376 -82
  531. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  532. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  533. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  534. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  535. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  536. package/neural-interface/public/shared/ui-detail.js +15 -3
  537. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  538. package/neural-interface/public/shared/ui-file-links.js +2 -2
  539. package/neural-interface/public/shared/ui-games.js +2 -2
  540. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  541. package/neural-interface/public/shared/ui-judgments.js +1 -1
  542. package/neural-interface/public/shared/ui-keybinds.js +33 -2
  543. package/neural-interface/public/shared/ui-link.js +3 -3
  544. package/neural-interface/public/shared/ui-menubar.js +10 -20
  545. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  546. package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
  547. package/neural-interface/public/shared/ui-navbar.js +49 -24
  548. package/neural-interface/public/shared/ui-notifications.js +1 -0
  549. package/neural-interface/public/shared/ui-panels.js +96 -65
  550. package/neural-interface/public/shared/ui-resume.js +9 -7
  551. package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
  552. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  553. package/neural-interface/public/shared/ui-sessions.js +27 -15
  554. package/neural-interface/public/shared/ui-settings.js +683 -526
  555. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  556. package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
  557. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  558. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  559. package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
  560. package/neural-interface/public/shared/ui-skills.js +33 -2830
  561. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  562. package/neural-interface/public/shared/ui-sync.js +15 -5
  563. package/neural-interface/public/shared/ui-terminal.js +270 -190
  564. package/neural-interface/public/shared/ui-tool-updates.js +6 -4
  565. package/neural-interface/public/shared/ui-tooltip.js +81 -51
  566. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  567. package/neural-interface/public/shared/ui-trash.js +6 -4
  568. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  569. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  570. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  571. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  572. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  573. package/neural-interface/public/shared/ui-update.js +38 -2
  574. package/neural-interface/public/shared/ui-whiteboard.js +312 -59
  575. package/neural-interface/public/shared/ui-worker-end.js +5 -1
  576. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  577. package/neural-interface/public/shared/view-variant.js +66 -0
  578. package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
  579. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  580. package/neural-interface/public/shared/worker-end.js +10 -3
  581. package/neural-interface/public/shared/worker-questions.js +34 -0
  582. package/neural-interface/public/shared/worker-restart.js +12 -0
  583. package/neural-interface/public/sidepanel-session.html +2 -2
  584. package/neural-interface/public/variant/2d/background.js +2 -1
  585. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  586. package/neural-interface/public/variant/2d/graph.js +42 -20
  587. package/neural-interface/public/variant/2d/hulls.js +14 -9
  588. package/neural-interface/public/variant/2d/lasso.js +11 -7
  589. package/neural-interface/public/variant/2d/main.js +32 -1
  590. package/neural-interface/public/variant/2d/minimap.js +11 -13
  591. package/neural-interface/public/variant/3d/main.js +69 -6
  592. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  593. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  594. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  595. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  596. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  597. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  598. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  599. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  600. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  601. package/neural-interface/scripts/build-vendor.mjs +32 -0
  602. package/neural-interface/server.js +4553 -2855
  603. package/neural-interface/templates/rulesets/README.md +1 -1
  604. package/neural-interface/templates/rulesets/core.md +1 -1
  605. package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
  606. package/neural-interface/templates/rulesets/manifest.json +8 -8
  607. package/npm-shrinkwrap.json +34 -12
  608. package/package.json +17 -3
  609. package/postinstall.js +9 -4
  610. package/preuninstall.js +1 -1
  611. package/setup.js +97 -322
  612. package/skills/skill-forge/SKILL.md +177 -0
  613. package/skills/skill-forge/evals/evals.json +54 -0
  614. package/skills/skill-forge/references/package.md +105 -0
  615. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  616. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  617. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  618. package/skills/synabun/SKILL.md +10 -7
  619. package/skills/synabun/modules/audit.md +1 -5
  620. package/skills/synabun/modules/changelog.md +1 -5
  621. package/skills/synabun/modules/idea.md +1 -1
  622. package/skills/synabun/modules/memorize.md +6 -9
  623. package/skills/synabun/modules/schedule.md +6 -6
  624. package/mcp-server/dist/services/profiles.d.ts +0 -120
  625. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  626. package/mcp-server/dist/services/profiles.js +0 -387
  627. package/mcp-server/dist/services/profiles.js.map +0 -1
  628. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  629. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  630. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  631. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  632. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  633. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  634. package/mcp-server/dist/tools/leonardo.js +0 -21
  635. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  636. package/mcp-server/dist/tools/profile.d.ts +0 -17
  637. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  638. package/mcp-server/dist/tools/profile.js +0 -114
  639. package/mcp-server/dist/tools/profile.js.map +0 -1
  640. package/mcp-server/src/services/profiles.ts +0 -428
  641. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  642. package/mcp-server/src/tools/leonardo.ts +0 -30
  643. package/mcp-server/src/tools/profile.ts +0 -122
  644. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  645. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  646. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  647. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  648. package/skills/leonardo/SKILL.md +0 -168
  649. package/skills/leonardo/icon.svg +0 -1
  650. package/skills/leonardo/modules/browser-guide.md +0 -378
  651. package/skills/leonardo/modules/image-prompter.md +0 -283
  652. package/skills/leonardo/modules/model-advisor.md +0 -100
  653. package/skills/leonardo/modules/prompt-library.md +0 -110
  654. package/skills/leonardo/modules/style-guide.md +0 -186
  655. package/skills/leonardo/modules/video-prompter.md +0 -317
  656. /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, 5)) keep.add(item.path);
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();