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
@@ -16,11 +16,18 @@ import { dirname, join, resolve } from 'node:path';
16
16
  import { homedir } from 'node:os';
17
17
  import {
18
18
  mergeGeminiMcpConfig,
19
- mergeOpenCodeMcpConfig,
20
19
  } from '../neural-interface/lib/mcp-client-config.js';
20
+ import {
21
+ mcpServersOf,
22
+ opencodeConfigDir,
23
+ opencodeConfigFilePath,
24
+ opencodeMcpServerEntry,
25
+ setMcpServer,
26
+ } from '../neural-interface/lib/opencode-config.js';
21
27
  import {
22
28
  inspectCodexConfig,
23
29
  upsertCodexMcpConfig,
30
+ upsertCodexMcpContent,
24
31
  } from '../mcp-server/dist/services/codex-config-heal.js';
25
32
  import { mcpStdioCommand } from './packaged-runtime.js';
26
33
 
@@ -60,11 +67,12 @@ export function auditAndRepairClientConfigs({
60
67
  } = {}) {
61
68
  if (!dataHome || !packageRoot) throw new Error('dataHome and packageRoot are required');
62
69
  const definition = buildCanonicalMcpDefinition({ dataHome, packageRoot });
70
+ // Codex loads every SynaBun tool (its deferred catalog); a Codex runtime's own list is only its focus.
63
71
  const codexDefinition = {
64
72
  ...definition,
65
73
  env: {
66
74
  ...definition.env,
67
- SYNABUN_PROFILE: 'full',
75
+ SYNABUN_TOOLS: 'all',
68
76
  SYNABUN_TOOL_CATALOG_MODE: 'deferred',
69
77
  },
70
78
  };
@@ -105,11 +113,12 @@ export function auditAndRepairClientConfigs({
105
113
  results.gemini = { exists: existsSync(geminiPath), connected: false, changed: false, path: geminiPath };
106
114
  }
107
115
 
108
- const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(home, '.config');
109
- const openCodePath = join(xdgConfigHome, 'opencode', 'config.json');
116
+ // OpenCode 2's global opencode.json (it never reads the config.json of OpenCode 1), its mcp.servers.SynaBun:
117
+ // the folder the rest of SynaBun writes (OPENCODE_CONFIG_DIR, else XDG_CONFIG_HOME's, else ~/.config/opencode).
118
+ const openCodePath = opencodeConfigFilePath(opencodeConfigDir({ home }));
110
119
  const openCode = readJson(openCodePath);
111
- if (openCode?.mcp?.SynaBun) {
112
- const next = mergeOpenCodeMcpConfig(openCode, 'SynaBun', definition);
120
+ if (mcpServersOf(openCode).SynaBun) {
121
+ const next = setMcpServer(structuredClone(openCode), 'SynaBun', opencodeMcpServerEntry(definition));
113
122
  const changed = jsonChanged(openCode, next);
114
123
  if (apply && changed) writeJsonAtomic(openCodePath, next);
115
124
  results.opencode = { exists: true, connected: true, changed, path: openCodePath };
@@ -126,9 +135,9 @@ export function auditAndRepairClientConfigs({
126
135
  upsertCodexMcpConfig(codexPath, codexDefinition);
127
136
  changed = before !== readFileSync(codexPath, 'utf-8');
128
137
  } else {
138
+ // What the repair would write, compared with what is there: a value like "all" is in most files anyway.
129
139
  const content = readFileSync(codexPath, 'utf-8');
130
- changed = ![codexDefinition.command, ...codexDefinition.args].every(value => content.includes(value))
131
- || !Object.values(codexDefinition.env).every(value => content.includes(value));
140
+ changed = upsertCodexMcpContent(content, codexDefinition) !== content;
132
141
  }
133
142
  results.codex = { exists: true, connected: true, changed, path: codexPath, health: codexHealth };
134
143
  } else {
@@ -193,9 +193,15 @@ export async function runAppBackend({
193
193
  // 3. Start, through the launcher (lock, supervisor record, background start).
194
194
  let launcherPid = null;
195
195
  let lastPhase = null;
196
+ let lastStep = null;
197
+ // A phase is said when it begins; the snapshot also says how far it is (its step, the bytes done), for the
198
+ // window's progress bar.
196
199
  const reportPhase = (current) => {
197
- if (!current?.phase || current.phase === lastPhase) return;
200
+ if (!current?.phase) return;
201
+ const step = current.phase === 'snapshot' ? `${current.detail?.step ?? ''}:${current.detail?.done ?? ''}:${current.detail?.state ?? ''}` : null;
202
+ if (current.phase === lastPhase && step === lastStep) return;
198
203
  lastPhase = current.phase;
204
+ lastStep = step;
199
205
  emit({ type: 'supervisor', phase: current.phase, detail: current.detail || null });
200
206
  };
201
207
  const reporter = () => ({
@@ -66,7 +66,7 @@ export const EXTERNAL_TOOLS = Object.freeze([
66
66
  label: 'OpenCode',
67
67
  command: 'opencode',
68
68
  packages: Object.freeze(['opencode-ai']),
69
- // (@opencode-ai/sdk is the JavaScript client and is not matched.)
69
+ // (No OpenCode client package ships: SynaBun speaks to OpenCode 2's serve itself, neural-interface/lib/opencode2-client.js.)
70
70
  patterns: Object.freeze([new RegExp(`^opencode-${PLATFORMS}-`)]),
71
71
  }),
72
72
  Object.freeze({
@@ -11,6 +11,16 @@
11
11
  // The launcher also leaves its stop message in the server's stderr log
12
12
  // (recordLauncherFailure), because a detached launch has no terminal: the
13
13
  // updater's relaunch reads it back to tell the user why SynaBun did not start.
14
+ //
15
+ // The snapshot is the backup service's upgrade snapshot (createUpgradeSnapshot):
16
+ // a consistent copy of each database (a clone where the file system clones)
17
+ // that passed SQLite's integrity check, stored uncompressed, checksummed as it
18
+ // is written and read back once. The desktop app takes it itself before it
19
+ // replaces its own files (lib/update-snapshot.js) and hands it over; this start
20
+ // uses it only when the data home is still what it was taken from
21
+ // (checkUpgradeSnapshotFreshness), whatever process wrote since: an AI tool's
22
+ // own MCP server may have saved a memory while the app swapped itself. Then it
23
+ // takes a new one, here, before the server and so any migration runs.
14
24
 
15
25
  import { appendFileSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
16
26
  import { resolve } from 'node:path';
@@ -31,19 +41,41 @@ function readJson(path, fallback) {
31
41
  /**
32
42
  * Why the snapshot an in-app update handed over cannot stand in for a fresh
33
43
  * one, or null when it can. It can only when the updater recorded that it
34
- * installed exactly the version now launching, and the archive still passes
35
- * verification. A `prepared` update is not enough: its updater may never have
36
- * run while the old server went on changing data.
44
+ * installed exactly the version now launching, the archive passes verification
45
+ * and its database copy passed SQLite's integrity check, and (the desktop app's
46
+ * snapshot, which records what it was taken from) nothing it protects changed
47
+ * since it was taken. A `prepared` update is not enough: its updater may never
48
+ * have run while the old server went on changing data.
37
49
  */
38
- async function handoffSnapshotProblem({ handoff, version, backupService }) {
50
+ async function handoffSnapshotProblem({ handoff, version, backupService, dataHome }) {
39
51
  if (handoff.status !== 'installed') return 'that update was prepared but never recorded as installed';
40
52
  if (typeof handoff.target !== 'string' || handoff.target !== version) return 'it belongs to an update to another version';
41
53
  if (typeof handoff.snapshotPath !== 'string' || !handoff.snapshotPath) return 'it names no snapshot';
42
54
  if (!existsSync(handoff.snapshotPath)) return 'its snapshot file is gone';
43
55
  try {
44
56
  const service = await backupService();
45
- const archive = await service.verifyBackupArchive(handoff.snapshotPath);
46
- if (archive?.kind !== 'pre-update') return 'its file is not a pre-update snapshot';
57
+ let manifest;
58
+ // The desktop app read its snapshot back once, right after writing it, and
59
+ // recorded which file that was. The very same file, untouched since (device,
60
+ // inode, size, modification time), is not read a second time: only its
61
+ // manifest. Any other file is read in full.
62
+ if (handoff.verifiedStamp && typeof service.sameBackupFile === 'function' && typeof service.readBackupArchiveManifest === 'function'
63
+ && service.sameBackupFile(handoff.snapshotPath, handoff.verifiedStamp)) {
64
+ manifest = await service.readBackupArchiveManifest(handoff.snapshotPath);
65
+ if (manifest?.verified !== true) return 'its file is not a verified snapshot';
66
+ } else {
67
+ manifest = await service.verifyBackupArchive(handoff.snapshotPath);
68
+ }
69
+ if (manifest?.kind !== 'pre-update') return 'its file is not a pre-update snapshot';
70
+ // The restore runs SQLite's integrity check on the database it puts back: a copy that never passed it is no copy.
71
+ if (manifest.database && manifest.database.integrity !== 'ok') return 'its database copy was not checked with SQLite\'s integrity check';
72
+ // The data as it is now, before anything of this version touches it. The desktop app's snapshot says what it was
73
+ // taken from; the npm updater's (taken by the old server right before it stopped) does not, and is used as before.
74
+ if (handoff.source === 'desktop' || manifest.inputs) {
75
+ if (typeof service.checkUpgradeSnapshotFreshness !== 'function') return 'this version cannot tell whether the data changed since';
76
+ const freshness = await service.checkUpgradeSnapshotFreshness({ dataHome, manifest, databasePath: resolve(dataHome, 'mcp-data', 'memory.db') });
77
+ if (!freshness?.fresh) return `the data changed after it was taken (${freshness?.reason || 'unknown'})`;
78
+ }
47
79
  } catch (error) {
48
80
  return `its snapshot did not pass verification (${error?.message || error})`;
49
81
  }
@@ -94,7 +126,10 @@ export async function protectFirstLaunchAfterUpdate({
94
126
  let snapshotPath = null;
95
127
  if (pendingHandoff) {
96
128
  info('Checking the snapshot taken by the updater...');
97
- const problem = await handoffSnapshotProblem({ handoff, version, backupService });
129
+ // The desktop app's splash says it: the copy made before the update is compared with the data (supervisor phase
130
+ // `snapshot`, step `check`).
131
+ progress({ step: 'check' });
132
+ const problem = await handoffSnapshotProblem({ handoff, version, backupService, dataHome });
98
133
  if (problem) info(`The updater's snapshot is not used: ${problem}. Taking a new one.`);
99
134
  else {
100
135
  snapshotPath = handoff.snapshotPath;
@@ -106,11 +141,16 @@ export async function protectFirstLaunchAfterUpdate({
106
141
  if (!snapshotPath) {
107
142
  const folderPath = resolve(dataHome, 'backups', 'updates');
108
143
  let service;
144
+ let lastPhase = null;
109
145
  try {
110
146
  info(`Protecting user state before first launch of v${version}...`);
111
147
  progress({});
112
148
  service = await backupService();
113
- const snapshot = await service.createVerifiedBackup({
149
+ // The upgrade snapshot where the service has it (every SynaBun since 2026-10-21); the full backup otherwise.
150
+ const takeSnapshot = typeof service.createUpgradeSnapshot === 'function'
151
+ ? options => service.createUpgradeSnapshot(options)
152
+ : options => service.createVerifiedBackup(options);
153
+ const snapshot = await takeSnapshot({
114
154
  dataHome,
115
155
  databasePath: resolve(dataHome, 'mcp-data', 'memory.db'),
116
156
  folderPath,
@@ -118,17 +158,22 @@ export async function protectFirstLaunchAfterUpdate({
118
158
  appVersion: manifest.lastAppVersion || null,
119
159
  // Without this the checksum and archive phases run silently for minutes
120
160
  // on a large data home, which users read as a hang and kill.
121
- onProgress: ({ phase, files, bytes, skippedFiles, skippedBytes }) => {
122
- progress({ step: phase, files, bytes, skippedFiles });
161
+ onProgress: ({ phase, files, bytes, skippedFiles, skippedBytes, done, total, method, state }) => {
162
+ progress({ step: phase, files, bytes, skippedFiles, done, total, ...(state ? { state } : {}) });
163
+ // Each phase is said once, when it begins; the steps within it only reach the progress bar.
164
+ if (phase === lastPhase) return;
165
+ lastPhase = phase;
123
166
  const size = bytes ? ` (${(bytes / 1024 / 1024).toFixed(0)} MB)` : '';
124
167
  if (phase === 'collect') {
125
168
  info(` ${files} files to protect${size}`);
126
169
  if (skippedFiles) {
127
170
  info(` Skipping ${skippedFiles} generated media files (${(skippedBytes / 1024 / 1024).toFixed(0)} MB) — an upgrade never touches them`);
128
171
  }
129
- } else if (phase === 'checksum') info(` Checksumming ${files} files...`);
130
- else if (phase === 'archive') info(` Compressing${size}...`);
172
+ } else if (phase === 'copy') info(` Copying the databases${method === 'clone' ? ' (a file system clone)' : ''}...`);
173
+ else if (phase === 'checksum') info(` Checksumming ${files} files...`);
174
+ else if (phase === 'archive') info(total ? ` Writing the archive (${(total / 1024 / 1024).toFixed(0)} MB)...` : ` Compressing${size}...`);
131
175
  else if (phase === 'verify') info(' Verifying archive integrity...');
176
+ else if (phase === 'integrity') info(' Finishing SQLite\'s integrity check of the database copies...');
132
177
  },
133
178
  });
134
179
  snapshotPath = snapshot.path;
@@ -0,0 +1,176 @@
1
+ // What "the MCP server is built" means, in one place: mcp-server/dist/index.js and every module it imports by a
2
+ // relative path, transitively, are on disk. mcp-server/dist/ is build output that git ignores, yet a checkout can
3
+ // hold a tracked dist/index.js whose newer imports were never committed (they are ignored too): a server started
4
+ // from it fails to load /mcp. postinstall.js and setup.js build when this says no; the Neural Interface's restore,
5
+ // onboarding status and build route, the install details, the release and the packaged app's checks ask it too.
6
+ //
7
+ // The imports are read from the code itself, never from a comment or a string: a light tokenizer skips comments,
8
+ // strings, template literals and regular expressions, so a word "import" in a text cannot ask for a module that is
9
+ // not loaded (a packaged app has no compiler: a false "missing" there would stop it). Only plain fs and path:
10
+ // postinstall.js and setup.js import this on any Node.
11
+
12
+ import { existsSync, readFileSync, statSync } from 'node:fs';
13
+ import { dirname, join, relative, resolve, sep } from 'node:path';
14
+
15
+ const MAX_MODULES = 5000;
16
+ const MAX_MODULE_BYTES = 8 * 1024 * 1024;
17
+ const CODE_EXT = /\.(?:m?js|cjs)$/i;
18
+ // After one of these a `/` starts a regular expression, after anything else it divides.
19
+ const REGEX_AFTER_PUNCT = new Set(['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '<', '>', '~', '^']);
20
+ const REGEX_AFTER_WORD = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'throw', 'case', 'do', 'else', 'yield', 'await']);
21
+
22
+ /**
23
+ * The tokens of a module that matter for its imports: words, punctuation and string literals (their values), with
24
+ * comments, template literal text and regular expressions left out.
25
+ * @returns {{ t: 'word'|'punct'|'string', v: string }[]}
26
+ */
27
+ export function codeTokens(text) {
28
+ const src = String(text ?? '');
29
+ const out = [];
30
+ const frames = []; // one per open `${` of a template literal: the brace depth that closes it
31
+ let depth = 0;
32
+ let i = 0;
33
+ const last = () => out[out.length - 1];
34
+ const regexAllowed = () => {
35
+ const p = last();
36
+ if (!p) return true;
37
+ if (p.t === 'punct') return REGEX_AFTER_PUNCT.has(p.v);
38
+ if (p.t === 'word') return REGEX_AFTER_WORD.has(p.v);
39
+ return false;
40
+ };
41
+ const template = () => {
42
+ // From just after a backtick (or a closing `}` of a `${`): to the closing backtick or the next `${`.
43
+ while (i < src.length) {
44
+ const c = src[i];
45
+ if (c === '\\') { i += 2; continue; }
46
+ if (c === '`') { i += 1; out.push({ t: 'string', v: '' }); return; }
47
+ if (c === '$' && src[i + 1] === '{') { i += 2; frames.push(depth); depth += 1; return; }
48
+ i += 1;
49
+ }
50
+ };
51
+ while (i < src.length) {
52
+ const c = src[i];
53
+ if (c === '/' && src[i + 1] === '/') { const nl = src.indexOf('\n', i); i = nl < 0 ? src.length : nl; continue; }
54
+ if (c === '/' && src[i + 1] === '*') { const end = src.indexOf('*/', i + 2); i = end < 0 ? src.length : end + 2; continue; }
55
+ if (c === '\'' || c === '"') {
56
+ let j = i + 1; let v = '';
57
+ while (j < src.length && src[j] !== c && src[j] !== '\n') { if (src[j] === '\\') { v += src[j + 1] ?? ''; j += 2; continue; } v += src[j]; j += 1; }
58
+ out.push({ t: 'string', v });
59
+ i = j + 1;
60
+ continue;
61
+ }
62
+ if (c === '`') { i += 1; template(); continue; }
63
+ if (c === '/' && regexAllowed()) {
64
+ let j = i + 1; let inClass = false;
65
+ while (j < src.length && src[j] !== '\n') {
66
+ const d = src[j];
67
+ if (d === '\\') { j += 2; continue; }
68
+ if (d === '[') inClass = true;
69
+ else if (d === ']') inClass = false;
70
+ else if (d === '/' && !inClass) break;
71
+ j += 1;
72
+ }
73
+ i = j + 1;
74
+ while (i < src.length && /[a-z]/i.test(src[i])) i += 1;
75
+ out.push({ t: 'punct', v: ')' }); // a value, as far as a following `/` is concerned
76
+ continue;
77
+ }
78
+ if (/[A-Za-z_$]/.test(c)) {
79
+ let j = i + 1;
80
+ while (j < src.length && /[\w$]/.test(src[j])) j += 1;
81
+ out.push({ t: 'word', v: src.slice(i, j) });
82
+ i = j;
83
+ continue;
84
+ }
85
+ if (/\s/.test(c)) { i += 1; continue; }
86
+ if (/[0-9]/.test(c)) { let j = i + 1; while (j < src.length && /[\w.]/.test(src[j])) j += 1; out.push({ t: 'word', v: src.slice(i, j) }); i = j; continue; }
87
+ if (c === '{') depth += 1;
88
+ if (c === '}') {
89
+ if (frames.length && depth - 1 === frames[frames.length - 1]) { depth -= 1; frames.pop(); i += 1; template(); continue; }
90
+ depth = Math.max(0, depth - 1);
91
+ }
92
+ out.push({ t: 'punct', v: c });
93
+ i += 1;
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /** The module specifiers a module loads: static imports, `export … from`, side-effect imports, `import('…')`. */
99
+ export function importSpecifiers(text) {
100
+ const tokens = codeTokens(text);
101
+ const found = [];
102
+ const at = (k) => tokens[k] || { t: 'end', v: '' };
103
+ for (let k = 0; k < tokens.length; k++) {
104
+ const tok = tokens[k];
105
+ if (tok.t !== 'word' || (tok.v !== 'import' && tok.v !== 'export')) continue;
106
+ if (k > 0 && at(k - 1).t === 'punct' && at(k - 1).v === '.') continue; // obj.import
107
+ const next = at(k + 1);
108
+ if (tok.v === 'import') {
109
+ if (next.t === 'punct' && next.v === '.') continue; // import.meta
110
+ if (next.t === 'punct' && next.v === '(') { if (at(k + 2).t === 'string' && at(k + 3).v === ')') found.push(at(k + 2).v); continue; }
111
+ if (next.t === 'string') { found.push(next.v); continue; }
112
+ }
113
+ // import a, { b as c } from '…'; import * as d from '…'; export { e } from '…'; export * from '…'
114
+ let j = k + 1;
115
+ while (j < tokens.length) {
116
+ const x = at(j);
117
+ if (x.t === 'word' && x.v === 'from') break;
118
+ if (x.t === 'word' || (x.t === 'punct' && (x.v === ',' || x.v === '{' || x.v === '}' || x.v === '*'))) { j += 1; continue; }
119
+ j = -1;
120
+ break;
121
+ }
122
+ if (j > k + 1 && at(j).v === 'from' && at(j + 1).t === 'string') {
123
+ if (tok.v === 'export' && !(next.v === '*' || next.v === '{')) continue;
124
+ found.push(at(j + 1).v);
125
+ }
126
+ }
127
+ return found;
128
+ }
129
+
130
+ const posix = (p) => p.split(sep).join('/');
131
+
132
+ /**
133
+ * Every module dist/index.js needs that is not on disk (or cannot be read), as paths from the package root.
134
+ * Modules inside mcp-server/dist are followed; one outside it (the package's lib/) is only looked for.
135
+ * @param {string} packageRoot
136
+ * @param {{ exists?: (p: string) => boolean, readText?: (p: string) => string, entry?: string }} [o]
137
+ * @returns {{ complete: boolean, missing: string[], checked: number }}
138
+ */
139
+ export function mcpDistMissing(packageRoot, { exists = existsSync, readText = null, entry = 'index.js' } = {}) {
140
+ const root = resolve(packageRoot);
141
+ const dist = join(root, 'mcp-server', 'dist');
142
+ const read = readText || ((p) => {
143
+ const st = statSync(p);
144
+ if (!st.isFile()) throw new Error('not a regular file');
145
+ if (st.size > MAX_MODULE_BYTES) return null;
146
+ return readFileSync(p, 'utf8');
147
+ });
148
+ const missing = new Set();
149
+ const seen = new Set();
150
+ const queue = [join(dist, entry)];
151
+ let checked = 0;
152
+ while (queue.length && checked < MAX_MODULES) {
153
+ const file = queue.shift();
154
+ if (seen.has(file)) continue;
155
+ seen.add(file);
156
+ if (!exists(file)) { missing.add(posix(relative(root, file))); continue; }
157
+ const inside = file.startsWith(dist + sep);
158
+ if (!inside || !CODE_EXT.test(file)) continue;
159
+ let text;
160
+ try { text = read(file); } catch { missing.add(posix(relative(root, file))); continue; }
161
+ checked += 1;
162
+ if (text === null) continue; // too large to read here: present, not followed
163
+ for (const spec of importSpecifiers(text)) {
164
+ if (!spec.startsWith('./') && !spec.startsWith('../')) continue;
165
+ const target = resolve(dirname(file), spec.split(/[?#]/)[0]);
166
+ if (!seen.has(target)) queue.push(target);
167
+ }
168
+ }
169
+ const list = [...missing].sort();
170
+ return { complete: list.length === 0, missing: list, checked };
171
+ }
172
+
173
+ /** Whether the MCP server is built: dist/index.js and everything it imports are there. */
174
+ export function mcpDistBuilt(packageRoot, options) {
175
+ return mcpDistMissing(packageRoot, options).complete;
176
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ /** Shared CI identity, OIDC-only publication and exact-byte handoff contract. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
5
+ import { createReadStream } from 'node:fs';
6
+ import { appendFile, lstat, readFile, readdir, writeFile } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { hashFile } from './virustotal.mjs';
10
+ import { publishNpm, runNpm, verifyPacked } from './release-publish.mjs';
11
+
12
+ export const REPOSITORY = 'danilokhury/Synabun';
13
+ export const WORKFLOW = '.github/workflows/publish.yml';
14
+ export const PUBLISH_NPM = '11.21.0'; // Verified OIDC-compatible CLI; builder stays 10.9.3.
15
+ export const stableVersion = value => typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
16
+ export const exactCommit = value => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
17
+ export const requestId = value => typeof value === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value);
18
+ export const runTitle = ({ version, commit, request }) => `npm ${version} ${commit} ${request}`;
19
+ export const artifactName = (id, attempt) => `npm-publication-${id}-${attempt}`;
20
+ const numberId = value => /^\d+$/.test(String(value)) && Number.isSafeInteger(Number(value)) && Number(value) > 0;
21
+
22
+ export function validateIdentity({ env, event, pkg, head, main }) {
23
+ if (env.GITHUB_REPOSITORY !== REPOSITORY || pkg.name !== 'synabun' || !stableVersion(pkg.version)
24
+ || pkg.repository?.url !== `git+https://github.com/${REPOSITORY}.git`) throw new Error('Unexpected npm package/repository identity');
25
+ if (!exactCommit(head) || head !== main || !numberId(env.GITHUB_RUN_ID) || !numberId(env.GITHUB_RUN_ATTEMPT)) throw new Error('Release commit must be the current main tip; require exact run identity');
26
+ let request = null;
27
+ if (env.GITHUB_EVENT_NAME === 'workflow_dispatch') {
28
+ if (env.GITHUB_REF !== 'refs/heads/main' || env.GITHUB_SHA !== head || event.inputs?.commit !== head
29
+ || event.inputs?.version !== pkg.version || !requestId(event.inputs?.request_id)) throw new Error('Invalid dispatch ref/commit/version/request identity');
30
+ request = event.inputs.request_id;
31
+ } else if (env.GITHUB_EVENT_NAME === 'release') {
32
+ const tag = event.release?.tag_name;
33
+ if (event.action !== 'published' || event.release?.draft !== false || event.release?.prerelease !== false
34
+ || !new RegExp(`^v?\\.?${pkg.version.replaceAll('.', '\\.')}$`).test(tag || '')
35
+ || env.GITHUB_REF !== `refs/tags/${tag}` || env.GITHUB_SHA !== head) throw new Error('Invalid stable release event/tag');
36
+ } else throw new Error('Only main workflow_dispatch or stable release.published can publish');
37
+ if (env.GITHUB_WORKFLOW_REF !== `${REPOSITORY}/${WORKFLOW}@${env.GITHUB_REF}`) throw new Error('Unexpected publisher workflow identity');
38
+ return { repository: REPOSITORY, workflow: WORKFLOW, event: env.GITHUB_EVENT_NAME,
39
+ version: pkg.version, commit: head, request, run_id: Number(env.GITHUB_RUN_ID), run_attempt: Number(env.GITHUB_RUN_ATTEMPT) };
40
+ }
41
+
42
+ export function oidcEnvironment(env, { cli, userconfig, globalconfig, cache }) {
43
+ if (env.RUNNER_ENVIRONMENT !== 'github-hosted' || !env.ACTIONS_ID_TOKEN_REQUEST_URL || !env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) throw new Error('GitHub-hosted OIDC runner and id-token: write are required');
44
+ // Modern npm tries OIDC first. Remove legacy credentials anyway: no silent token fallback.
45
+ const clean = Object.fromEntries(Object.entries(env).filter(([key]) => !/^(NPM|NPM_TOKEN|NODE_AUTH_TOKEN)$/i.test(key) && !/^npm_config_/i.test(key)));
46
+ return { ...clean, npm_execpath: cli, NPM_CONFIG_USERCONFIG: userconfig, NPM_CONFIG_GLOBALCONFIG: globalconfig,
47
+ NPM_CONFIG_CACHE: cache, NPM_CONFIG_REGISTRY: 'https://registry.npmjs.org/', NPM_CONFIG_UPDATE_NOTIFIER: 'false' };
48
+ }
49
+
50
+ export function verifyToolchain(node, npm) {
51
+ if (node.replace(/^v/, '') !== '22.19.0' || npm.trim() !== PUBLISH_NPM) throw new Error(`Publication requires Node 22.19.0 and isolated npm ${PUBLISH_NPM}; builder npm is unchanged`);
52
+ }
53
+
54
+ export async function registryVersion(version, { fetchImpl = fetch, allowMissing = false } = {}) {
55
+ if (!stableVersion(version)) throw new Error('Invalid registry version');
56
+ const response = await fetchImpl(`https://registry.npmjs.org/synabun/${version}`, { signal: AbortSignal.timeout(30000) });
57
+ if (response.status === 404 && allowMissing) return null;
58
+ if (!response.ok) throw new Error(`npm registry lookup failed (${response.status})`);
59
+ const pkg = await response.json();
60
+ if (pkg?.name !== 'synabun' || pkg.version !== version || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(pkg.dist?.integrity || '')) throw new Error('npm registry identity/integrity is unverified');
61
+ return pkg.dist.integrity;
62
+ }
63
+
64
+ export async function sha512(path) {
65
+ const hash = createHash('sha512');
66
+ for await (const bytes of createReadStream(path)) hash.update(bytes);
67
+ return `sha512-${hash.digest('base64')}`;
68
+ }
69
+ export const bundleFiles = version => ['npm-virustotal.json', 'npm-virustotal.md', `synabun-${version}.tgz`];
70
+
71
+ export async function publicationReceipt({ out, identity, registry = registryVersion }) {
72
+ const packed = await verifyPacked({ out });
73
+ if (packed.tarball !== join(out, `synabun-${identity.version}.tgz`)) throw new Error('npm receipt version mismatch');
74
+ const integrity = await sha512(packed.tarball);
75
+ if (await registry(identity.version) !== integrity) throw new Error('npm registry SHA512 differs from scanned tarball');
76
+ const files = {};
77
+ for (const name of bundleFiles(identity.version)) files[name] = { sha256: await hashFile(join(out, name)), size: (await lstat(join(out, name))).size };
78
+ const receipt = { schema: 1, ...identity, integrity, files };
79
+ await writeFile(join(out, 'npm-publication.json'), JSON.stringify(receipt, null, 2) + '\n');
80
+ return receipt;
81
+ }
82
+
83
+ export async function verifyPublication({ out, expected, registry = registryVersion, strictFiles = true }) {
84
+ const receipt = JSON.parse(await readFile(join(out, 'npm-publication.json'), 'utf8'));
85
+ if (receipt.schema !== 1 || receipt.repository !== REPOSITORY || receipt.workflow !== WORKFLOW
86
+ || !stableVersion(receipt.version) || !exactCommit(receipt.commit) || !numberId(receipt.run_id)
87
+ || !numberId(receipt.run_attempt) || !['workflow_dispatch', 'release'].includes(receipt.event)
88
+ || (receipt.event === 'workflow_dispatch' && !requestId(receipt.request))) throw new Error('Unknown or invalid npm publication receipt');
89
+ for (const [key, value] of Object.entries(expected)) if (receipt[key] !== value) throw new Error(`npm publication identity mismatch: ${key}`);
90
+ const names = bundleFiles(expected.version);
91
+ if (JSON.stringify(Object.keys(receipt.files || {}).sort()) !== JSON.stringify([...names].sort())
92
+ || (strictFiles && JSON.stringify((await readdir(out)).sort()) !== JSON.stringify([...names, 'npm-publication.json'].sort()))) throw new Error('Unexpected npm artifact file set');
93
+ for (const name of [...names, 'npm-publication.json']) {
94
+ const info = await lstat(join(out, name));
95
+ if (!info.isFile() || info.isSymbolicLink()) throw new Error('npm artifact must contain regular files only');
96
+ if (name !== 'npm-publication.json' && (info.size !== receipt.files[name].size || await hashFile(join(out, name)) !== receipt.files[name].sha256)) throw new Error(`npm artifact integrity mismatch: ${name}`);
97
+ }
98
+ const packed = await verifyPacked({ out });
99
+ if (packed.tarball !== join(out, `synabun-${expected.version}.tgz`) || await sha512(packed.tarball) !== receipt.integrity
100
+ || await registry(expected.version) !== receipt.integrity) throw new Error('npm publication version/registry integrity mismatch');
101
+ // Read only the packed manifest; never extract arbitrary paths into the checkout.
102
+ const tar = spawnSync('tar', ['-xOf', packed.tarball, 'package/package.json'], { encoding: 'utf8', maxBuffer: 1024 * 1024, timeout: 30000 });
103
+ if (tar.status !== 0) throw new Error('Cannot inspect npm tarball manifest');
104
+ const pkg = JSON.parse(tar.stdout);
105
+ if (pkg.name !== 'synabun' || pkg.version !== expected.version || pkg.repository?.url !== `git+https://github.com/${REPOSITORY}.git`) throw new Error('Packed npm manifest identity mismatch');
106
+ return receipt;
107
+ }
108
+
109
+ function git(...args) {
110
+ const result = spawnSync('git', args, { encoding: 'utf8', timeout: 120000 });
111
+ if (result.status !== 0) throw new Error('Cannot verify current main commit');
112
+ return result.stdout.trim();
113
+ }
114
+ async function ciIdentity() {
115
+ git('fetch', '--no-tags', '--depth=1', 'origin', 'main');
116
+ return validateIdentity({ env: process.env, event: JSON.parse(await readFile(process.env.GITHUB_EVENT_PATH, 'utf8')),
117
+ pkg: JSON.parse(await readFile('package.json', 'utf8')), head: git('rev-parse', 'HEAD'), main: git('rev-parse', 'FETCH_HEAD') });
118
+ }
119
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
120
+ try {
121
+ const stage = process.argv[2], identity = await ciIdentity(), out = join(process.env.RUNNER_TEMP, 'npm-release');
122
+ if (stage === 'identity') {
123
+ const published = await registryVersion(identity.version, { allowMissing: true });
124
+ if (published && identity.event === 'workflow_dispatch') throw new Error('Version already published: recover the original successful run artifacts; never repack an existing version');
125
+ await appendFile(process.env.GITHUB_OUTPUT, `published=${!!published}\n`);
126
+ } else if (stage === 'publish') {
127
+ try { await lstat('.npmrc'); throw new Error('Project .npmrc is forbidden in the publishing checkout'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
128
+ const base = process.env.RUNNER_TEMP;
129
+ const userconfig = join(base, 'npm-oidc-userconfig'), globalconfig = join(base, 'npm-oidc-globalconfig');
130
+ await writeFile(userconfig, ''); await writeFile(globalconfig, '');
131
+ const env = oidcEnvironment(process.env, { cli: join(base, 'npm-oidc-cli/node_modules/npm/bin/npm-cli.js'), userconfig, globalconfig, cache: join(base, 'npm-oidc-cache') });
132
+ const run = (args, options) => runNpm(args, { ...options, env });
133
+ verifyToolchain(process.version, run(['--version'], { capture: true }));
134
+ await publishNpm({ cwd: process.cwd(), out, run, publishPacked: true, provenance: true });
135
+ await publicationReceipt({ out, identity });
136
+ } else throw new Error('Use identity or publish');
137
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
138
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /** The only documented npm publication path: check, pack, scan, publish the same tgz. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import { access, chmod, mkdir, mkdtemp, readFile, copyFile, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import { approved, credentials, evidenceDigest, hashFile } from './virustotal.mjs';
9
+ import { scanRelease } from './release-scan.mjs';
10
+
11
+ export function runNpm(args, { cwd, env = process.env, capture = false } = {}) {
12
+ // npm_execpath works with npm.cmd on Windows without shell argument interpolation.
13
+ const npm = env.npm_execpath;
14
+ if (!npm && process.platform === 'win32') throw new Error('On Windows run npm run release:publish so npm_execpath is available');
15
+ const command = npm ? process.execPath : 'npm';
16
+ const result = spawnSync(command, npm ? [npm, ...args] : args, { cwd, env, encoding: 'utf8', stdio: capture ? 'pipe' : 'inherit', timeout: 1800000, maxBuffer: 8 * 1024 * 1024 });
17
+ // Never replay npm's captured output (it may contain authentication diagnostics).
18
+ if (result.error || result.status !== 0) throw new Error(`npm ${args[0]} failed; publication stopped`);
19
+ return result.stdout;
20
+ }
21
+ export async function verifyPacked({ out, now = Date.now() }) {
22
+ const report = JSON.parse(await readFile(join(out, 'npm-virustotal.json'), 'utf8'));
23
+ if (report.schema !== 1 || !report.approved || report.items?.length !== 1 || !approved(report.items)) throw new Error('npm scan report is not approved');
24
+ const item = report.items[0];
25
+ if (item.kind !== 'file' || basename(item.name) !== item.name || !/^synabun-.*\.tgz$/.test(item.name)) throw new Error('Invalid npm scan subject');
26
+ const { evidence_digest, review, ...evidence } = item;
27
+ if (evidenceDigest(evidence) !== evidence_digest) throw new Error('npm report evidence digest mismatch');
28
+ const at = Date.parse(item.analysis_date);
29
+ if (!Number.isFinite(at) || at > now + 300000 || now - at > 7 * 86400000 || item.analysis_status !== 'completed' || item.supported_engines < 10) throw new Error('npm scan report stale, unfinished or inconclusive');
30
+ if (item.status === 'detections') {
31
+ const { applyReviews } = await import('./virustotal.mjs');
32
+ if (!approved(applyReviews([{ ...item, review: null }], [{ ...review, name: item.name, sha256: item.sha256, evidence_digest }], now))) throw new Error('npm detection review required');
33
+ }
34
+ const tarball = join(out, item.name);
35
+ if (await hashFile(tarball) !== item.sha256) throw new Error('npm tarball SHA-256 mismatch; repack and scan');
36
+ return { tarball, sha256: item.sha256 };
37
+ }
38
+ export async function publishNpm({ cwd, out, reviewFile, provenance = false, packOnly = false, publishPacked = false, run = runNpm, scan = scanRelease }) {
39
+ credentials(); // Before installs or registry mutations.
40
+ const rel = relative(resolve(cwd), resolve(out));
41
+ if (!rel || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\'))) throw new Error('Release output must be outside the checkout to keep reports/tarballs out of npm packages');
42
+ const pkg = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8'));
43
+ if (pkg.name !== 'synabun' || pkg.scripts?.prepublishOnly?.includes('refuse-publish-from-dev') || !pkg.scripts?.prepublishOnly?.includes('release-publish.mjs guard')) throw new Error('Publish from a fresh prepared Clean clone with npm run release:publish; Dev publication is refused');
44
+ await mkdir(out, { recursive: true });
45
+ if (!publishPacked) {
46
+ run(['run', 'deps:check'], { cwd });
47
+ run(['ci', '--ignore-scripts'], { cwd });
48
+ // Preserve Clean's entire install/build/file/pack chain. npm publish TGZ skips it.
49
+ run(['run', 'prepublishOnly'], { cwd, env: { ...process.env, SYNABUN_RELEASE_PACK: '1' } });
50
+ run(['run', 'prepack'], { cwd });
51
+ for (const file of ['mcp-server/dist/index.js', 'NOTICE', 'THIRD-PARTY-LICENSES.md']) await access(join(cwd, file));
52
+ const staging = await mkdtemp(join(tmpdir(), 'synabun-npm-pack-'));
53
+ try {
54
+ const packed = JSON.parse(run(['pack', '--ignore-scripts', '--json', '--pack-destination', staging], { cwd, capture: true }));
55
+ const name = packed?.[0]?.filename;
56
+ if (packed.length !== 1 || !name || basename(name) !== name || name !== `synabun-${pkg.version}.tgz`) throw new Error('npm pack returned an unexpected tarball');
57
+ const tarball = join(out, name);
58
+ await rm(tarball, { force: true });
59
+ await copyFile(join(staging, name), tarball);
60
+ await chmod(tarball, 0o444);
61
+ await scan({ files: [tarball], out, prefix: 'npm-virustotal', reviewFile });
62
+ } finally { await rm(staging, { recursive: true, force: true }); }
63
+ }
64
+ const packed = await verifyPacked({ out });
65
+ if (basename(packed.tarball) !== `synabun-${pkg.version}.tgz`) throw new Error('npm tarball version mismatch');
66
+ if (!packOnly) {
67
+ run(['publish', packed.tarball, '--ignore-scripts', '--access', 'public', '--tag', 'latest', ...(provenance ? ['--provenance'] : [])], { cwd });
68
+ if (await hashFile(packed.tarball) !== packed.sha256) throw new Error('npm tarball changed during publication; inspect registry immediately');
69
+ }
70
+ return packed;
71
+ }
72
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
73
+ try {
74
+ if (process.argv[2] === 'guard') {
75
+ if (process.env.SYNABUN_RELEASE_PACK !== '1') throw new Error('Direct npm publish is disabled: use npm run release:publish -- --out /absolute/release/folder (packs, scans and publishes the exact tgz).');
76
+ } else {
77
+ const args = process.argv.slice(2); let out, reviewFile, provenance = false, packOnly = false, publishPacked = false;
78
+ while (args.length) {
79
+ const arg = args.shift();
80
+ if (arg === '--out') out = args.shift();
81
+ else if (arg === '--review') reviewFile = args.shift();
82
+ else if (arg === '--provenance') provenance = true;
83
+ else if (arg === '--pack-only') packOnly = true;
84
+ else if (arg === '--publish-packed') publishPacked = true;
85
+ else throw new Error(`Unknown option: ${arg}`);
86
+ }
87
+ if (!out || (packOnly && publishPacked)) throw new Error('Use --out DIR; --pack-only and --publish-packed are mutually exclusive');
88
+ await publishNpm({ cwd: dirname(dirname(fileURLToPath(import.meta.url))), out: resolve(out), reviewFile, provenance, packOnly, publishPacked });
89
+ }
90
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
91
+ }