synabun 2.0.0 → 2.0.7

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 (507) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/NOTICE +5 -0
  3. package/README.md +2 -1
  4. package/THIRD-PARTY-LICENSES.md +59 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +153 -18
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +26 -3
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/child-path.js +303 -0
  14. package/lib/claude-hooks.js +2 -1
  15. package/lib/data-home-migration.js +2 -0
  16. package/lib/desktop-app.js +265 -0
  17. package/lib/external-tools.js +7 -3
  18. package/lib/log-redact.js +102 -0
  19. package/lib/packaged-runtime.js +138 -11
  20. package/lib/start-launcher.js +88 -1
  21. package/mcp-server/dist/http.d.ts +3 -1
  22. package/mcp-server/dist/http.d.ts.map +1 -1
  23. package/mcp-server/dist/http.js +15 -4
  24. package/mcp-server/dist/http.js.map +1 -1
  25. package/mcp-server/dist/index.d.ts.map +1 -1
  26. package/mcp-server/dist/index.js +9 -6
  27. package/mcp-server/dist/index.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  33. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  34. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  35. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  36. package/mcp-server/dist/services/identity.d.ts +11 -0
  37. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  38. package/mcp-server/dist/services/identity.js +24 -0
  39. package/mcp-server/dist/services/identity.js.map +1 -1
  40. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  41. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  42. package/mcp-server/dist/services/neural-interface.js +34 -2
  43. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  44. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  45. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  46. package/mcp-server/dist/services/typesafe-config.js +4 -1
  47. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  48. package/mcp-server/dist/tools/agents.d.ts +10 -7
  49. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  50. package/mcp-server/dist/tools/agents.js +34 -13
  51. package/mcp-server/dist/tools/agents.js.map +1 -1
  52. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  53. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  54. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  55. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  56. package/mcp-server/dist/tui.js +2 -1
  57. package/mcp-server/dist/tui.js.map +1 -1
  58. package/mcp-server/dist/version.d.ts +5 -0
  59. package/mcp-server/dist/version.d.ts.map +1 -0
  60. package/mcp-server/dist/version.js +21 -0
  61. package/mcp-server/dist/version.js.map +1 -0
  62. package/mcp-server/package-lock.json +2 -2
  63. package/mcp-server/package.json +1 -3
  64. package/mcp-server/run.mjs +15 -9
  65. package/mcp-server/src/http.ts +18 -3
  66. package/mcp-server/src/index.ts +9 -6
  67. package/mcp-server/src/services/budget-judgments.ts +183 -0
  68. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  69. package/mcp-server/src/services/identity.ts +27 -0
  70. package/mcp-server/src/services/neural-interface.ts +34 -2
  71. package/mcp-server/src/services/typesafe-config.ts +4 -1
  72. package/mcp-server/src/tools/agents.ts +33 -14
  73. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  74. package/mcp-server/src/tui.ts +2 -1
  75. package/mcp-server/src/version.ts +22 -0
  76. package/neural-interface/i18n/en.json +2126 -209
  77. package/neural-interface/i18n/pt-BR.json +2123 -207
  78. package/neural-interface/lib/account-usage.js +163 -0
  79. package/neural-interface/lib/assistant-api.js +316 -57
  80. package/neural-interface/lib/assistant-brains/claude.js +19 -2
  81. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  82. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  83. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  84. package/neural-interface/lib/assistant-budget-split.js +408 -0
  85. package/neural-interface/lib/assistant-budget.js +94 -95
  86. package/neural-interface/lib/assistant-catalog.js +179 -27
  87. package/neural-interface/lib/assistant-config.js +91 -14
  88. package/neural-interface/lib/assistant-dispatch.js +494 -138
  89. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  90. package/neural-interface/lib/assistant-jev.js +26 -3
  91. package/neural-interface/lib/assistant-memory.js +24 -10
  92. package/neural-interface/lib/assistant-persona.js +73 -12
  93. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  94. package/neural-interface/lib/assistant-pricing.js +21 -6
  95. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  96. package/neural-interface/lib/assistant-route-gate.js +19 -3
  97. package/neural-interface/lib/assistant-router.js +373 -42
  98. package/neural-interface/lib/assistant-runtime.js +488 -59
  99. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  100. package/neural-interface/lib/assistant-test-density.js +142 -0
  101. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  102. package/neural-interface/lib/assistant-user-context.js +908 -0
  103. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  104. package/neural-interface/lib/assistant-user-learn.js +404 -0
  105. package/neural-interface/lib/assistant-user-threads.js +226 -0
  106. package/neural-interface/lib/assistant-user-writer.js +183 -0
  107. package/neural-interface/lib/augmented-path.js +317 -53
  108. package/neural-interface/lib/browser-background-tab.js +134 -0
  109. package/neural-interface/lib/browser-screenshot.js +678 -61
  110. package/neural-interface/lib/claude-accounts.js +342 -31
  111. package/neural-interface/lib/claude-agent-bridge.js +123 -9
  112. package/neural-interface/lib/claude-executable.js +6 -1
  113. package/neural-interface/lib/claude-model-catalog.js +33 -5
  114. package/neural-interface/lib/claude-panel-auth.js +221 -0
  115. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  116. package/neural-interface/lib/claude-temporary.js +46 -7
  117. package/neural-interface/lib/cli-install-api.js +137 -0
  118. package/neural-interface/lib/cli-install.js +797 -0
  119. package/neural-interface/lib/cli-path.js +130 -0
  120. package/neural-interface/lib/cli-resolver.js +744 -0
  121. package/neural-interface/lib/cli-update-planner.js +143 -7
  122. package/neural-interface/lib/cloudflared/install.js +533 -0
  123. package/neural-interface/lib/cloudflared/manager.js +545 -0
  124. package/neural-interface/lib/cloudflared/pins.js +115 -0
  125. package/neural-interface/lib/cloudflared/pins.json +63 -0
  126. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  127. package/neural-interface/lib/codex-account-auth.js +178 -0
  128. package/neural-interface/lib/codex-accounts.js +489 -0
  129. package/neural-interface/lib/codex-capabilities.js +2 -1
  130. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  131. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  132. package/neural-interface/lib/codex-panel-auth.js +62 -0
  133. package/neural-interface/lib/codex-session-settings.js +1 -1
  134. package/neural-interface/lib/codex-settings-schema.js +120 -0
  135. package/neural-interface/lib/desktop/config.js +3 -0
  136. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  137. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  138. package/neural-interface/lib/editor-completion.js +714 -0
  139. package/neural-interface/lib/editor-config.js +149 -0
  140. package/neural-interface/lib/editor-files-api.js +268 -0
  141. package/neural-interface/lib/editor-git-api.js +134 -0
  142. package/neural-interface/lib/file-explorer-api.js +1039 -0
  143. package/neural-interface/lib/file-explorer-system.js +355 -0
  144. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  145. package/neural-interface/lib/file-explorer-watch.js +239 -0
  146. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  147. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  148. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  149. package/neural-interface/lib/git-bash/install.js +529 -0
  150. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  151. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  152. package/neural-interface/lib/git-bash/manager.js +268 -0
  153. package/neural-interface/lib/git-bash/pins.js +206 -0
  154. package/neural-interface/lib/git-bash/pins.json +34 -0
  155. package/neural-interface/lib/git-bash/resolver.js +194 -0
  156. package/neural-interface/lib/git-diff-api.js +103 -0
  157. package/neural-interface/lib/git-read.js +234 -0
  158. package/neural-interface/lib/greeting/api.js +89 -0
  159. package/neural-interface/lib/greeting/budgets.js +90 -0
  160. package/neural-interface/lib/greeting/engine.js +461 -0
  161. package/neural-interface/lib/greeting/facts.js +320 -0
  162. package/neural-interface/lib/greeting/sources.js +252 -0
  163. package/neural-interface/lib/greeting/writer.js +139 -0
  164. package/neural-interface/lib/http-guards.js +189 -13
  165. package/neural-interface/lib/install-details.js +145 -0
  166. package/neural-interface/lib/machine-names.js +88 -0
  167. package/neural-interface/lib/mcp-installer.js +14 -2
  168. package/neural-interface/lib/memory-api.js +31 -7
  169. package/neural-interface/lib/native-loop-providers.js +26 -8
  170. package/neural-interface/lib/native-loop-runtime.js +9 -4
  171. package/neural-interface/lib/os-path.js +319 -0
  172. package/neural-interface/lib/plugin-installer.js +4 -2
  173. package/neural-interface/lib/project-storage.js +5 -7
  174. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  175. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  176. package/neural-interface/lib/pty-host/core.js +2 -1
  177. package/neural-interface/lib/pty-host/manager.js +27 -2
  178. package/neural-interface/lib/remote-policy.js +20 -11
  179. package/neural-interface/lib/request-gate.js +589 -0
  180. package/neural-interface/lib/session-title-generator.js +4 -3
  181. package/neural-interface/lib/system-about.js +340 -0
  182. package/neural-interface/lib/system-activity.js +66 -0
  183. package/neural-interface/lib/terminal-env.js +11 -3
  184. package/neural-interface/lib/terminal-path.js +55 -0
  185. package/neural-interface/lib/ui-blob-api.js +108 -0
  186. package/neural-interface/lib/ui-blob-store.js +68 -0
  187. package/neural-interface/lib/virtual-socket.js +12 -1
  188. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  189. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  190. package/neural-interface/lib/whatsapp/commands.js +13 -14
  191. package/neural-interface/lib/whatsapp/config.js +134 -20
  192. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  193. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  194. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  195. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  196. package/neural-interface/lib/whatsapp/manager.js +92 -32
  197. package/neural-interface/lib/whatsapp/paths.js +11 -1
  198. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  199. package/neural-interface/lib/whatsapp/redact.js +63 -0
  200. package/neural-interface/lib/whatsapp/service.js +343 -91
  201. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  202. package/neural-interface/package-lock.json +701 -2
  203. package/neural-interface/package.json +13 -2
  204. package/neural-interface/public/index.html +2 -0
  205. package/neural-interface/public/index2d.html +2 -0
  206. package/neural-interface/public/install-app.html +9 -6
  207. package/neural-interface/public/offline.html +19 -3
  208. package/neural-interface/public/onboarding.html +107 -2
  209. package/neural-interface/public/shared/account-i18n.js +7 -0
  210. package/neural-interface/public/shared/account-menu.js +539 -0
  211. package/neural-interface/public/shared/account-usage.js +19 -0
  212. package/neural-interface/public/shared/account-view.js +335 -0
  213. package/neural-interface/public/shared/agent-runtime-options.js +50 -14
  214. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  215. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  216. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  217. package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
  218. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  219. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  220. package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
  221. package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
  222. package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
  223. package/neural-interface/public/shared/assistant/asst-render.js +197 -23
  224. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  225. package/neural-interface/public/shared/assistant/asst-route.js +95 -343
  226. package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
  227. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  228. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  229. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  230. package/neural-interface/public/shared/assistant/asst-state.js +186 -2
  231. package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
  232. package/neural-interface/public/shared/blob-store.js +111 -22
  233. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  234. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  235. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  236. package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
  237. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  238. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  239. package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
  240. package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
  241. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  242. package/neural-interface/public/shared/claude-account-view.js +3 -0
  243. package/neural-interface/public/shared/cli-install-view.js +392 -0
  244. package/neural-interface/public/shared/cli-status.js +282 -22
  245. package/neural-interface/public/shared/constants.js +6 -1
  246. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  247. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  248. package/neural-interface/public/shared/cp/cp-styles.js +61 -0
  249. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  250. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  251. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  252. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  253. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  254. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  255. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  256. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  257. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  258. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  259. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  260. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  261. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  262. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  263. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  264. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  265. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  266. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  267. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  268. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  269. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  270. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  271. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  272. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  273. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  274. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  275. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  276. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  277. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  278. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  279. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  280. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  281. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  282. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  283. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  284. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  285. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  286. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  287. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  288. package/neural-interface/public/shared/file-links.js +339 -0
  289. package/neural-interface/public/shared/git-bash-view.js +145 -0
  290. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  291. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  292. package/neural-interface/public/shared/html-shell.js +161 -165
  293. package/neural-interface/public/shared/i18n.js +19 -1
  294. package/neural-interface/public/shared/install-details-view.js +86 -0
  295. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  296. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  297. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
  298. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  299. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
  300. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
  301. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  302. package/neural-interface/public/shared/pwa-install-page.js +32 -2
  303. package/neural-interface/public/shared/pwa-install.css +3 -3
  304. package/neural-interface/public/shared/pwa-install.js +100 -1
  305. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  306. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  307. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  308. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  309. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  310. package/neural-interface/public/shared/settings/settings-ia.js +33 -4
  311. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  312. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  313. package/neural-interface/public/shared/start-bridge.js +238 -19
  314. package/neural-interface/public/shared/storage.js +30 -3
  315. package/neural-interface/public/shared/styles.css +1710 -770
  316. package/neural-interface/public/shared/synabun-mascot.js +68 -58
  317. package/neural-interface/public/shared/tunnel-view.js +167 -0
  318. package/neural-interface/public/shared/ui-claude-panel.js +369 -229
  319. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  320. package/neural-interface/public/shared/ui-detail.js +15 -0
  321. package/neural-interface/public/shared/ui-explorer.js +22 -0
  322. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  323. package/neural-interface/public/shared/ui-file-links.js +103 -0
  324. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  325. package/neural-interface/public/shared/ui-help.js +26 -0
  326. package/neural-interface/public/shared/ui-invite.js +126 -116
  327. package/neural-interface/public/shared/ui-judgments.js +10 -1
  328. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  329. package/neural-interface/public/shared/ui-loading.js +187 -19
  330. package/neural-interface/public/shared/ui-menubar.js +25 -33
  331. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  332. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  333. package/neural-interface/public/shared/ui-navbar.js +48 -22
  334. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  335. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  336. package/neural-interface/public/shared/ui-sessions.js +9 -3
  337. package/neural-interface/public/shared/ui-settings.js +509 -490
  338. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  339. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  340. package/neural-interface/public/shared/ui-sync.js +22 -0
  341. package/neural-interface/public/shared/ui-terminal.js +40 -5
  342. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  343. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  344. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  345. package/neural-interface/public/shared/ui-update.js +594 -5
  346. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  347. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  348. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  349. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  350. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  351. package/neural-interface/public/shared/worker-end.js +253 -0
  352. package/neural-interface/public/sw.js +2 -2
  353. package/neural-interface/public/variant/2d/main.js +10 -1
  354. package/neural-interface/public/variant/3d/main.js +10 -1
  355. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  356. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  357. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  358. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  359. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  360. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  361. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  362. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  363. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  364. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  365. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  366. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  367. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  368. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  369. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  370. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  371. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  372. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  373. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  374. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  375. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  376. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  377. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  378. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  379. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  380. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  381. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  382. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  383. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  384. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  385. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  386. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  387. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  388. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  389. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  390. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  391. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  392. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  393. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  394. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  395. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  396. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  397. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  398. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  399. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  400. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  401. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  402. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  403. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  404. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  405. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  406. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  407. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  408. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  409. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  410. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  411. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  412. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  413. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  414. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  415. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  416. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  417. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  418. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  419. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  420. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  421. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  422. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  423. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  424. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  425. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  426. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  427. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  428. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  429. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  430. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  431. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  432. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  433. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  434. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  435. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  436. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  437. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  438. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  439. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  440. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  441. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  442. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  443. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  444. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  445. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  446. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  447. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  448. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  449. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  450. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  451. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  452. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  453. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  454. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  455. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  456. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  457. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  458. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  459. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  460. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  461. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  462. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  463. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  464. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  465. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  466. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  467. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  468. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  469. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  470. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  471. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  472. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  473. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  474. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  475. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  476. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  477. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  478. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  479. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  480. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  481. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  482. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  483. package/neural-interface/scripts/build-vendor.mjs +78 -2
  484. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  485. package/neural-interface/server.js +2085 -1629
  486. package/neural-interface/templates/rulesets/README.md +1 -1
  487. package/package.json +5 -8
  488. package/setup.js +53 -15
  489. package/benchmarks/browser-live-results.json +0 -79
  490. package/benchmarks/browser-v2-results.json +0 -258
  491. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  492. package/neural-interface/public/category-logos/claude.svg +0 -1
  493. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  494. package/neural-interface/public/category-logos/openai.svg +0 -1
  495. package/neural-interface/public/category-logos/synabun.png +0 -0
  496. package/neural-interface/public/claude.svg +0 -1
  497. package/neural-interface/public/gallery.svg +0 -6
  498. package/neural-interface/public/leonardoai.svg +0 -1
  499. package/neural-interface/public/mcp.svg +0 -4
  500. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  501. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  502. package/neural-interface/public/sessionicon.svg +0 -8
  503. package/neural-interface/public/toolbar-album.svg +0 -8
  504. package/neural-interface/public/toolbar-question.svg +0 -6
  505. package/neural-interface/public/toolbar-sessions.svg +0 -6
  506. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  507. package/skills/synabun/synabunicon.png +0 -0
@@ -13,6 +13,14 @@
13
13
  // owner's messages; a synabun.mailbox event is RUNNING(mailbox), a turn the
14
14
  // CLI starts itself RUNNING(background), and a turn nobody here submitted (a
15
15
  // prompt typed on the desktop) RUNNING(external) — tracked, never forwarded.
16
+ // A task the router continues on another model (a synabun.continuation event:
17
+ // a route decided automatically, by a remembered route, or on a card) answers
18
+ // where its prompt came from: RUNNING(followup) for a prompt from the phone,
19
+ // whose answer is sent once as the reply (not an update, whatever "Forward
20
+ // background results" says), RUNNING(external) for any other. A follow-up is
21
+ // how a turn's answer is delivered, nothing more: what may run unasked or be
22
+ // approved is the runtime's (a continuation is its own turn, never the owner's
23
+ // message: computer use asks once in it) and the owner's typed answers.
16
24
  // The turn object exists before submit is called: brain events can arrive
17
25
  // before it resolves.
18
26
  //
@@ -23,8 +31,9 @@
23
31
  // closes WITHOUT being granted (denied, cancelled) and the message goes to the
24
32
  // Assistant as the next prompt. An approval is only ever granted by a "yes"
25
33
  // the bridge matched on a message the owner typed, aimed at that request while
26
- // it is open: never by the model, by a forwarded message, by a reply that
27
- // quotes something else or by anything that merely looks like an answer.
34
+ // it is open: never by the model, by a forwarded message, by a voice note (its
35
+ // transcript is an ordinary message, `voice: true`), by a reply that quotes
36
+ // something else or by anything that merely looks like an answer.
28
37
  // Messages that arrive while a turn runs wait as one merged prompt and go in
29
38
  // as soon as it ends; a queue watch lets go of a turn whose end never came.
30
39
  // A request that appears while such a message waits closes at once, unseen and
@@ -87,14 +96,28 @@ export const BRIDGE_LIMITS = Object.freeze({
87
96
  policyTickMs: 30_000,
88
97
  });
89
98
 
99
+ /**
100
+ * Why a voice note was not transcribed (the host's unsupported.reason), told to the phone once per
101
+ * reason every unsupportedEveryMs. Anything else unsupported gets the generic line.
102
+ */
103
+ export const AUDIO_NOT_TRANSCRIBED = Object.freeze({
104
+ transcription_disabled: 'Voice notes are not transcribed: that is turned off in Settings → WhatsApp on your computer. Type your message instead.',
105
+ transcription_unavailable: 'I can\'t transcribe voice notes yet: this computer needs whisper.cpp, its speech model and FFmpeg (Settings → WhatsApp says what is missing). Type your message instead.',
106
+ too_large: 'That voice note is too large for me to transcribe (20 MB at most). Send a shorter one, or type it.',
107
+ too_long: 'That voice note is too long for me to transcribe (10 minutes at most). Send a shorter one, or type it.',
108
+ stale: 'That voice note was sent a while ago, so I did not transcribe it. Send it again if you still need it.',
109
+ transcription_failed: 'I couldn\'t transcribe that voice note. Try again, or type your message.',
110
+ });
111
+
90
112
  /** Send refusals that mean "not now": the reply waits in the outbox for the next connect. */
91
113
  const RESEND_ON_CONNECT = /DISCONNECT|NOT_CONNECTED|OFFLINE|RECONNECT|CLOSED|TIMEOUT|HOST_UNAVAILABLE/i;
92
114
  /**
93
115
  * What happens to a reply the transport refused: 'connect' (it waits for the
94
116
  * next connect), 'later' (THROTTLED: sent again after its retryAfterMs) or
95
- * 'drop'. PAUSED (an owner anomaly paused sending) and HELD (the connector
96
- * kept crashing) drop it: the service records why in the activity log and the
97
- * Settings tab shows the state.
117
+ * 'drop'. PAUSED (an owner anomaly paused sending) and HELD (a connector
118
+ * process of another SynaBun version, down until a restart; a crash is not
119
+ * HELD: it reads HOST_UNAVAILABLE and the reply waits) drop it: the service
120
+ * records why in the activity log and the Settings tab shows the state.
98
121
  */
99
122
  export function outboxPlan(code) {
100
123
  const value = String(code || '');
@@ -104,7 +127,7 @@ export function outboxPlan(code) {
104
127
 
105
128
  const DEFAULT_CONFIG = Object.freeze({
106
129
  enabled: true, level: 'ask', progress: 'key', forwardBackground: true, replyLabel: null, maxMessages: 3,
107
- rotation: 'idle6h', strictWorkerApprovals: false, computerUse: false, mode: 'self', paused: false, pausedBy: null, autonomousUntil: null,
130
+ rotation: 'idle6h', strictWorkerApprovals: false, computerUse: false, mode: 'self', paused: false, pausedBy: null,
108
131
  });
109
132
  const ALREADY_ANSWERED = new Set(['CONTROL_UNKNOWN', 'ROUTE_ALREADY_DECIDED', 'ROUTE_NOT_FOUND', 'CLARIFY_ALREADY_ANSWERED', 'CLARIFY_NOT_FOUND', 'PERMISSION_ALREADY_RESOLVED', 'NO_PENDING_REQUEST', 'RUN_NOT_FOUND']);
110
133
  const ROUTE_DONE_PHASES = new Set(['decided', 'auto', 'expired', 'declined', 'cancelled']);
@@ -191,6 +214,8 @@ export function createWhatsAppBridge({
191
214
  outboxTimer: null, // a THROTTLED reply's retry
192
215
  staleSkipped: 0,
193
216
  staleTimer: null,
217
+ stoppedAt: 0, // when the owner's last /stop was sent (its message time)
218
+ stopLateNoted: false, // one note per /stop for what was sent before it and arrived after it
194
219
  unsupportedAt: new Map(),
195
220
  minuteWindow: { start: 0, count: 0, noticed: false },
196
221
  dayWindow: { start: 0, count: 0, noticed: false },
@@ -435,14 +460,16 @@ export function createWhatsAppBridge({
435
460
  state.brainApplied = { sessionId, signature };
436
461
  persistBridgeState();
437
462
  }
463
+ // Autonomous has no time limit on WhatsApp: the policy carries no autonomousUntil (lib/remote-policy.js effectiveLevel).
464
+ // WhatsApp turned off on this computer (`enabled: false`, which also stops the connector) ends computer use like the switch.
438
465
  function remoteFromConfig(cfg = readConfig()) {
439
- return { channel: 'whatsapp', level: cfg.level || 'ask', strictWorkerApprovals: cfg.strictWorkerApprovals === true, autonomousUntil: cfg.autonomousUntil || null, paused: state.paused, computerUse: cfg.computerUse === true && state.computerEnded !== true };
466
+ return { channel: 'whatsapp', level: cfg.level || 'ask', strictWorkerApprovals: cfg.strictWorkerApprovals === true, paused: state.paused, computerUse: cfg.computerUse === true && cfg.enabled !== false && state.computerEnded !== true };
440
467
  }
441
468
  /** The policy of one of our sessions: computer use is the current conversation's alone. */
442
469
  function remoteFor(id, base = remoteFromConfig()) {
443
470
  return id === state.sessionId ? base : { ...base, computerUse: false };
444
471
  }
445
- const policySignature = (remote) => JSON.stringify([remote.level, remote.strictWorkerApprovals, remote.autonomousUntil, remote.paused, remote.computerUse]);
472
+ const policySignature = (remote) => JSON.stringify([remote.level, remote.strictWorkerApprovals, remote.paused, remote.computerUse]);
446
473
  async function createSession() {
447
474
  const rt = runtime();
448
475
  if (!rt?.createSession) throw Object.assign(new Error('The Assistant is not available right now.'), { code: 'RUNTIME_UNAVAILABLE' });
@@ -500,9 +527,8 @@ export function createWhatsAppBridge({
500
527
  // ── policy ─────────────────────────────────────────────────────────────────
501
528
  /**
502
529
  * Keep the level of every WhatsApp session in line with the config (level,
503
- * strict approvals, the autonomous window, pause). A change — or an
504
- * autonomous window that just ran out — re-registers, which makes the
505
- * runtime re-apply the clamp and stop runs above it.
530
+ * strict approvals, pause, computer use). A change re-registers, which makes
531
+ * the runtime re-apply the clamp and stop runs above it.
506
532
  */
507
533
  function syncPolicies() {
508
534
  const cfg = readConfig();
@@ -652,7 +678,8 @@ export function createWhatsAppBridge({
652
678
  if (turn.kind === 'followup') state.followUp = null;
653
679
  else { presence('paused'); state.inflight = null; persistBridgeState(); react(turn.sourceIds, 'done'); }
654
680
  } else if (turn.kind === 'followup') {
655
- // The task the phone decided on a card: its answer is a reply, not an update.
681
+ // The task the phone decided on a card, or that its prompt carried on to another model: its answer
682
+ // is a reply, not an update. Said once: the turn is gone already, a repeated end finds nothing.
656
683
  state.followUp = null;
657
684
  const text = finalText(turn);
658
685
  if (outcome === 'failed') await say(`Something went wrong: ${String(message || turn.resultError || 'the turn failed').slice(0, 400)}`, { maxChunks: 1 });
@@ -767,6 +794,12 @@ export function createWhatsAppBridge({
767
794
  // Our own prompt (the WhatsApp turn exists already); any other is someone else's turn.
768
795
  if (ev.origin !== 'whatsapp' && !state.turns.has(sessionId)) makeTurn(sessionId, 'external');
769
796
  return;
797
+ case 'synabun.continuation':
798
+ // The router carries a task on (another model, the same conversation), before any of its output.
799
+ // A prompt from the phone: its answer is the reply to the phone. Anything else stays as before:
800
+ // someone else's turn, unless a card the phone saw was just decided (ensureTurn).
801
+ if (!state.turns.has(sessionId)) ensureTurn(sessionId, ev.origin === 'whatsapp' ? 'followup' : 'external');
802
+ return;
770
803
  case 'synabun.mailbox': {
771
804
  const items = Array.isArray(ev.items) ? ev.items : [];
772
805
  const turn = state.turns.get(sessionId);
@@ -1031,6 +1064,10 @@ export function createWhatsAppBridge({
1031
1064
  case 'help': await say(HELP_TEXT, { maxChunks: 2 }); return;
1032
1065
  case 'status': await say(statusText(statusSnapshot()), { maxChunks: 1 }); return;
1033
1066
  case 'stop': {
1067
+ // What was sent before this /stop and arrives after it (a voice note still being transcribed, and
1068
+ // what waited behind it) is not acted on, as /stop clears what already waits in line.
1069
+ state.stoppedAt = Math.max(state.stoppedAt, tsMs(message.ts, now()));
1070
+ state.stopLateNoted = false;
1034
1071
  const { stoppedRuns, clearedQueue } = await stopEverything({ reason: 'phone' });
1035
1072
  const n = stoppedRuns + clearedQueue;
1036
1073
  await say(`Stopped. Cancelled ${n} task${n === 1 ? '' : 's'}.`, { maxChunks: 1 });
@@ -1067,18 +1104,14 @@ export function createWhatsAppBridge({
1067
1104
  function statusSnapshot() {
1068
1105
  const view = state.sessionId ? sessionView(state.sessionId) : null;
1069
1106
  const turn = state.sessionId ? state.turns.get(state.sessionId) : null;
1070
- const rt = runtime();
1071
- let brainCapUsd = null;
1072
- try { brainCapUsd = rt?.brainCapUsd?.() ?? null; } catch { brainCapUsd = null; }
1073
1107
  const cfg = readConfig();
1074
1108
  const current = currentCard();
1075
1109
  return {
1076
1110
  connected: isConnected(), sessionId: state.sessionId, title: view?.title || null, brain: view?.brain || null,
1077
1111
  running: !!turn || !!view?.running, turnKind: turn?.kind || null, queued: state.queue.length,
1078
1112
  pendingCards: state.cards.size, currentCard: current ? (current.kind === 'route' ? 'where to run a task' : current.kind === 'computer' ? 'controlling your Mac' : current.kind) : null,
1079
- budget: view?.budget || null, brainUsd: view?.costUsd ?? null, brainCapUsd,
1113
+ budget: view?.budget || null, brainUsd: view?.costUsd ?? null,
1080
1114
  level: state.sessionId ? levelOf(state.sessionId) : cfg.level, paused: state.paused, pausedBy: state.pausedBy,
1081
- autonomousUntil: cfg.autonomousUntil || null,
1082
1115
  };
1083
1116
  }
1084
1117
  /** /stop and stopAll: the turn, the session's runs, open cards and the queue. */
@@ -1168,6 +1201,16 @@ export function createWhatsAppBridge({
1168
1201
  // `backlog`): never acted on, only counted for one note. Checked first, so a
1169
1202
  // backlog neither uses up the rate limits nor counts as activity.
1170
1203
  if (now() - tsMs(message.ts, now()) > L.staleMs) { noteStale(); return; }
1204
+ // Sent before the owner's last /stop, delivered after it (a typed /stop does not wait for a voice note
1205
+ // being transcribed): /stop already stopped it. A command still runs. What WhatsApp held while SynaBun was
1206
+ // offline arrives in order and is never overtaken (a backlog is never sent ahead), so it is not affected.
1207
+ if (state.stoppedAt && message.backlog !== true && tsMs(message.ts, now()) < state.stoppedAt && !parseCommand(message)) {
1208
+ if (!state.stopLateNoted) {
1209
+ state.stopLateNoted = true;
1210
+ await say('A message you sent before /stop arrived after it, so I did not act on it. Send it again if you still need it.', { maxChunks: 1 });
1211
+ }
1212
+ return;
1213
+ }
1171
1214
  if (rateLimited()) return;
1172
1215
  const activityBefore = state.lastActivityAt;
1173
1216
  state.lastActivityAt = now();
@@ -1178,10 +1221,12 @@ export function createWhatsAppBridge({
1178
1221
  const images = Array.isArray(message.images) ? message.images : [];
1179
1222
  if (message.unsupported && !text.trim() && !images.length) {
1180
1223
  const type = String(message.unsupported.type || 'this kind of');
1181
- const last = state.unsupportedAt.get(type) || 0;
1224
+ const audio = type === 'audio' && Object.hasOwn(AUDIO_NOT_TRANSCRIBED, String(message.unsupported.reason)) ? AUDIO_NOT_TRANSCRIBED[message.unsupported.reason] : null;
1225
+ const key = audio ? `audio:${message.unsupported.reason}` : type;
1226
+ const last = state.unsupportedAt.get(key) || 0;
1182
1227
  if (now() - last >= L.unsupportedEveryMs) {
1183
- state.unsupportedAt.set(type, now());
1184
- await say(`I can read text and pictures for now; ${type} messages are not supported yet.`, { maxChunks: 1 });
1228
+ state.unsupportedAt.set(key, now());
1229
+ await say(audio || `I can read text and pictures for now; ${type} messages are not supported yet.`, { maxChunks: 1 });
1185
1230
  }
1186
1231
  return;
1187
1232
  }
@@ -1198,8 +1243,8 @@ export function createWhatsAppBridge({
1198
1243
  }
1199
1244
  // Something is waiting for the owner: a clear answer answers it; anything else closes it
1200
1245
  // without a grant and carries on as the next prompt. Only words the owner typed can answer:
1201
- // never a forwarded message, a picture or "//text".
1202
- const answerable = !images.length && command?.kind !== 'literal' && message.forwarded !== true;
1246
+ // never a forwarded message, a picture, a voice note (its transcript) or "//text".
1247
+ const answerable = !images.length && command?.kind !== 'literal' && message.forwarded !== true && message.voice !== true;
1203
1248
  if (await replyToPending(message, { answerable })) return;
1204
1249
  if (rotationDue(activityBefore)) state.rotateOnNextSubmit = true;
1205
1250
  // A request raised between that look and here is older than this message too (onCard only sees
@@ -1271,7 +1316,7 @@ export function createWhatsAppBridge({
1271
1316
  if (!list.length) return;
1272
1317
  let composed = null;
1273
1318
  try { composed = inbound?.composePrompt ? inbound.composePrompt(list, { rng }) : null; } catch (error) { log('whatsapp:bridge', `compose: ${error?.message || error}`); }
1274
- if (!composed) composed = { text: list.map((part) => String(part.text || '')).filter(Boolean).join('\n\n'), images: list.flatMap((part) => part.images || []), sourceIds: list.map((part) => part.id).filter(Boolean), untrusted: list.some((part) => part.forwarded || part.quoted) };
1319
+ if (!composed) composed = { text: list.map((part) => String(part.text || '')).filter(Boolean).join('\n\n'), images: list.flatMap((part) => part.images || []), sourceIds: list.map((part) => part.id).filter(Boolean), untrusted: list.some((part) => part.forwarded || part.quoted || part.audioFile === true) };
1275
1320
  const images = Array.isArray(composed.images) ? composed.images.filter(Boolean) : [];
1276
1321
  const job = {
1277
1322
  text: String(composed.text || ''), images,
@@ -1402,22 +1447,21 @@ export function createWhatsAppBridge({
1402
1447
  }
1403
1448
  persistBridgeState();
1404
1449
  react(job.sourceIds, 'failed');
1405
- if (code === 'SESSION_BUDGET_EXCEEDED' || code === 'BRAIN_BUDGET_EXCEEDED') { await say(budgetText(code, sessionId, result.message), { maxChunks: 1 }); return; }
1450
+ if (code === 'SESSION_BUDGET_EXCEEDED' || code === 'BRAIN_SHARE_EXHAUSTED') { await say(budgetText(code, sessionId, result.message), { maxChunks: 1 }); return; }
1406
1451
  if (code === 'EMPTY_PROMPT') return;
1407
1452
  await say(`Could not start that: ${String(result.message || code).slice(0, 400)}`, { maxChunks: 1 });
1408
1453
  }
1409
- /** Spend against the cap, and the two ways on: /new or a higher cap. Never a silent new session. */
1454
+ /** Spend against the budget, and the two ways on: /new or a higher budget. Never a silent new session. */
1410
1455
  function budgetText(code, sessionId, fallback) {
1411
1456
  const view = sessionView(sessionId);
1457
+ const budget = view?.budget || null;
1412
1458
  let line = '';
1413
- if (code === 'SESSION_BUDGET_EXCEEDED' && view?.budget && Number.isFinite(Number(view.budget.hardUsd))) {
1414
- line = `This conversation spent ${usd(view.budget.totalUsd)} of its ${usd(view.budget.hardUsd)} cap.`;
1415
- } else if (code === 'BRAIN_BUDGET_EXCEEDED') {
1416
- let cap = null;
1417
- try { cap = runtime()?.brainCapUsd?.() ?? null; } catch { cap = null; }
1418
- line = cap !== null ? `The Assistant spent ${usd(view?.costUsd)} of its ${usd(cap)} cap in this conversation.` : '';
1459
+ if (code === 'SESSION_BUDGET_EXCEEDED' && budget && Number.isFinite(Number(budget.amountUsd))) {
1460
+ line = `This conversation spent ${usd(budget.totalUsd)} of its ${usd(budget.amountUsd)} budget.`;
1461
+ } else if (code === 'BRAIN_SHARE_EXHAUSTED' && budget && Number.isFinite(Number(budget.amountUsd))) {
1462
+ line = `The Assistant's share of this conversation's ${usd(budget.amountUsd)} budget is used up.`;
1419
1463
  }
1420
- return `${line || String(fallback || 'This conversation reached its budget cap.')} Send /new to start a fresh conversation, or raise the cap in SynaBun.`;
1464
+ return `${line || String(fallback || 'This conversation reached its budget.')} Send /new to start a fresh conversation, or raise the budget (or choose No limit) in SynaBun.`;
1421
1465
  }
1422
1466
 
1423
1467
  // ── lifecycle ──────────────────────────────────────────────────────────────
@@ -1484,14 +1528,14 @@ export function createWhatsAppBridge({
1484
1528
  persistBridgeState();
1485
1529
  }
1486
1530
 
1487
- // Keep the level in line with the config (Settings edits, the autonomous window running out).
1531
+ // Keep the level in line with the config (a change that reached it without a refresh).
1488
1532
  state.policyTimer = T.setInterval(() => { try { runtime(); syncPolicies(); } catch {} }, L.policyTickMs);
1489
1533
  state.policyTimer?.unref?.();
1490
1534
  runtime();
1491
1535
 
1492
1536
  return {
1493
1537
  onInbound, onConnection, status, newSession, pause, resume, stopAll, shutdown,
1494
- // Settings changed (level, strict approvals, autonomous window): re-apply now instead of at the next tick.
1538
+ // Settings changed (level, strict approvals, computer use): re-apply now instead of at the next tick.
1495
1539
  refresh: () => { state.policySeen.clear(); runtime(); syncPolicies(); return status(); },
1496
1540
  endComputerUse,
1497
1541
  _internals: { state, finalText, syncPolicies, drain, onPacket, brainTarget, moveOn },
@@ -2,10 +2,10 @@
2
2
  // SynaBun — WhatsApp Link: the owner's slash commands
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
- // Pure parsing and fixed texts. A command is the owner's own text — never a
6
- // forwarded or quoted message — that starts with one of COMMANDS
7
- // (case-insensitive). Commands run at once, busy or paused. Any other "/x" is
8
- // a prompt; "//x" sends the literal "/x". No command raises the level or
5
+ // Pure parsing and fixed texts. A command is the owner's own typed text —
6
+ // never a forwarded or quoted message, never a voice note — that starts with
7
+ // one of COMMANDS (case-insensitive). Commands run at once, busy or paused.
8
+ // Any other "/x" is a prompt; "//x" sends the literal "/x". No command raises the level or
9
9
  // changes a setting: those names get a fixed refusal (they would otherwise
10
10
  // reach the brain's CLI as its own slash commands).
11
11
 
@@ -21,7 +21,7 @@ const REFUSED = new Set([
21
21
 
22
22
  export const HELP_TEXT = [
23
23
  '*SynaBun on WhatsApp*',
24
- 'Write to me like you would in SynaBun. Pictures work too.',
24
+ 'Write to me like you would in SynaBun. Pictures work too, and voice notes when this computer can transcribe them.',
25
25
  '',
26
26
  '/status — what I am doing, spend and level',
27
27
  '/stop — stop the current task, its agents and anything waiting for your answer',
@@ -31,7 +31,7 @@ export const HELP_TEXT = [
31
31
  '/cards — ask again what is still waiting for your answer',
32
32
  '/help — this list',
33
33
  '',
34
- 'When I ask something, just answer in your own words. An approval takes a plain yes or no; any other message approves nothing and carries on the conversation. Start with // to send text that begins with a slash.',
34
+ 'When I ask something, just answer in your own words. An approval takes a plain yes or no, typed; any other message (a voice note too) approves nothing and carries on the conversation. Start with // to send text that begins with a slash.',
35
35
  'Levels, models and other settings change only in SynaBun on your computer (Settings → WhatsApp).',
36
36
  ].join('\n');
37
37
 
@@ -40,10 +40,10 @@ export const SETTINGS_REFUSAL = 'Settings change only in SynaBun on your compute
40
40
  /**
41
41
  * An inbound message → { kind: 'command', name, args } | { kind: 'refused', name }
42
42
  * | { kind: 'literal', text } ("//x" → "/x") | null (not a command: a prompt).
43
- * Only the owner's own text counts: forwarded or quoted messages are prompts.
43
+ * Only the owner's own typed text counts: forwarded or quoted messages and voice notes are prompts.
44
44
  */
45
45
  export function parseCommand(message) {
46
- if (!message || message.owner === false || message.forwarded || message.quoted) return null;
46
+ if (!message || message.owner === false || message.forwarded || message.quoted || message.voice === true) return null;
47
47
  const text = String(message.text ?? '').trim();
48
48
  if (!text.startsWith('/')) return null;
49
49
  if (text.startsWith('//')) return { kind: 'literal', text: text.slice(1) };
@@ -66,8 +66,8 @@ function levelLabel(level) {
66
66
 
67
67
  /**
68
68
  * /status text from a snapshot: { connected, sessionId, title, brain:{provider,model},
69
- * running, turnKind, queued, pendingCards, currentCard, budget:{totalUsd, hardUsd},
70
- * brainUsd, brainCapUsd, level, paused, pausedBy, autonomousUntil }.
69
+ * running, turnKind, queued, pendingCards, currentCard, budget:{ unlimited, totalUsd, amountUsd, leftUsd },
70
+ * brainUsd, level, paused, pausedBy }. Autonomous has no time limit: it holds until it is turned off in Settings.
71
71
  */
72
72
  export function statusText(s = {}) {
73
73
  const lines = ['*SynaBun status*'];
@@ -83,10 +83,9 @@ export function statusText(s = {}) {
83
83
  : 'idle';
84
84
  lines.push(`Now: ${doing}${s.queued ? ` · ${s.queued} message${s.queued === 1 ? '' : 's'} queued` : ''}`);
85
85
  if (s.pendingCards) lines.push(`Waiting for you: ${s.pendingCards} question${s.pendingCards === 1 ? '' : 's'} or approval${s.pendingCards === 1 ? '' : 's'}${s.currentCard ? ` (${s.currentCard})` : ''} — /cards asks again`);
86
- if (s.budget && Number.isFinite(Number(s.budget.hardUsd))) lines.push(`Spend: ${usd(s.budget.totalUsd)} of ${usd(s.budget.hardUsd)} session cap${Number.isFinite(Number(s.brainCapUsd)) ? ` (brain ${usd(s.brainUsd)} of ${usd(s.brainCapUsd)})` : ''}`);
87
- let level = `Level: ${levelLabel(s.level)}`;
88
- if (s.level === 'autonomous' && s.autonomousUntil) level += ` until ${new Date(Number(s.autonomousUntil)).toTimeString().slice(0, 5)}`;
89
- lines.push(level);
86
+ if (s.budget?.unlimited === true) lines.push(`Spend: ${usd(s.budget.totalUsd)} (no limit)`);
87
+ else if (s.budget && Number.isFinite(Number(s.budget.amountUsd))) lines.push(`Spend: ${usd(s.budget.totalUsd)} of the ${usd(s.budget.amountUsd)} budget${Number.isFinite(Number(s.brainUsd)) ? ` (brain ${usd(s.brainUsd)})` : ''}`);
88
+ lines.push(`Level: ${levelLabel(s.level)}${s.level === 'autonomous' ? ' until turned off' : ''}`);
90
89
  if (s.paused) lines.push(`Paused${s.pausedBy === 'desktop' ? ' from your computer' : ''} — ${s.pausedBy === 'desktop' ? 'resume it in SynaBun' : 'send /resume'}`);
91
90
  return lines.join('\n');
92
91
  }
@@ -10,12 +10,17 @@
10
10
  // Two kinds of fields:
11
11
  // user what Settings edits (update(), validated, versioned: a stale
12
12
  // expectedVersion is a 409 like lib/assistant-config.js)
13
- // service what SynaBun owns (mode, the conversation, the bound owner, the
14
- // autonomous window, pause) — written by the service and the
15
- // bridge through write(), refused in update().
13
+ // service what SynaBun owns (mode, the conversation, the bound owner,
14
+ // pause) — written by the service and the bridge through
15
+ // write(), refused in update().
16
16
  // Raising the level to 'autonomous' never applies here: update() needs
17
17
  // confirmEscalation and answers {pending:true}; the service applies it after
18
- // the phone answers "ALLOW <code>". Lowering applies at once.
18
+ // the phone answers "ALLOW <code>". Lowering applies at once. Autonomous then
19
+ // stays on until the owner lowers it in Settings, restarts included: there is
20
+ // no time limit. `autonomousUntil` is a legacy field (the end of the 8-hour
21
+ // window of 2.0.5 and before): a level 'autonomous' stored with one was turned
22
+ // on under that rule and is read as 'ask' (migrateLegacyAutonomous), so an
23
+ // update never puts anyone back into Autonomous.
19
24
  // `computerUse` is the owner's switch for controlling this Mac from a
20
25
  // WhatsApp conversation: off by default, a plain boolean, changed only through
21
26
  // update() (Settings on this computer: the guarded PUT /api/whatsapp/config).
@@ -25,9 +30,19 @@
25
30
  // Assistant) or { provider, model, effort }. Its shape is checked here; the
26
31
  // service checks the model and the effort against the Assistant's catalog
27
32
  // before it calls update() (lib/whatsapp/brain.js).
33
+ // Voice notes (lib/whatsapp/transcription.js): `transcriptionEnabled`, the
34
+ // spoken `transcriptionLanguage` ('auto' or a 2-3 letter code) and three
35
+ // paths, '' meaning "find it by itself": the whisper-cli program, the ggml
36
+ // model and FFmpeg. A path is the absolute path of an existing regular file
37
+ // (a program executable), checked when update() stores it and again by the
38
+ // host right before it runs; it is never looked up on PATH. Only update()
39
+ // (Settings on this computer) sets a path: a service write may only clear one,
40
+ // so nothing but the person at this computer chooses what the host runs.
28
41
 
29
42
  import { LEVELS } from '../remote-policy.js';
30
43
  import { cleanBrainChoice } from './brain.js';
44
+ import { TRANSCRIPTION_LANGUAGE_RE } from './protocol.js';
45
+ import { checkToolPath } from './transcription.js';
31
46
 
32
47
  export const WHATSAPP_CONFIG_KEY = 'whatsapp_config';
33
48
 
@@ -44,6 +59,11 @@ export const USER_DEFAULTS = Object.freeze({
44
59
  strictWorkerApprovals: false,
45
60
  computerUse: false,
46
61
  brain: null,
62
+ transcriptionEnabled: true,
63
+ transcriptionLanguage: 'auto',
64
+ transcriptionEnginePath: '',
65
+ transcriptionModelPath: '',
66
+ transcriptionFfmpegPath: '',
47
67
  });
48
68
 
49
69
  export const SERVICE_DEFAULTS = Object.freeze({
@@ -51,7 +71,7 @@ export const SERVICE_DEFAULTS = Object.freeze({
51
71
  sessionId: null,
52
72
  previousSessionIds: Object.freeze([]),
53
73
  owner: null, // { masked, boundAt, via } — never a number
54
- autonomousUntil: null, // epoch ms while an autonomous window is open
74
+ autonomousUntil: null, // legacy: the end of 2.0.5's 8-hour Autonomous window; always null now (migrateLegacyAutonomous)
55
75
  paused: false,
56
76
  pausedBy: null, // 'desktop' | 'phone'
57
77
  version: 0,
@@ -68,6 +88,11 @@ export const ROTATIONS = Object.freeze(['idle6h', 'daily', 'never']);
68
88
  export const SELF_TRIGGERS = Object.freeze(['all', 'prefix']);
69
89
  export const MODES = Object.freeze(['self', 'dedicated']);
70
90
  export const REPLY_LABEL_MAX = 24;
91
+ /** The voice-note settings the host gets (service prefsOf → protocol.js PREFS_SPEC). */
92
+ export const TRANSCRIPTION_FIELDS = Object.freeze(['transcriptionEnabled', 'transcriptionLanguage', 'transcriptionEnginePath', 'transcriptionModelPath', 'transcriptionFfmpegPath']);
93
+ /** The path fields and what each must name (transcription.js checkToolPath). */
94
+ export const TOOL_PATH_FIELDS = Object.freeze({ transcriptionEnginePath: 'program', transcriptionModelPath: 'file', transcriptionFfmpegPath: 'program' });
95
+ const TOOL_PATH_MAX = 4096;
71
96
 
72
97
  const RANK = Object.freeze({ 'read-only': 0, ask: 1, autonomous: 2 });
73
98
 
@@ -81,6 +106,28 @@ function configError(field, message, { code = 'CONFIG_INVALID', status = 400 } =
81
106
  return error;
82
107
  }
83
108
 
109
+ /** A tool path's shape: '' (find it by itself) or an absolute path of one line; undefined when it has neither shape. */
110
+ function cleanToolPath(value) {
111
+ if (value === null || value === undefined) return '';
112
+ if (typeof value !== 'string') return undefined;
113
+ const text = value.trim();
114
+ if (!text) return '';
115
+ if (text.length > TOOL_PATH_MAX || /[\u0000-\u001f\u007f]/.test(text)) return undefined;
116
+ return checkToolPath(text, 'program', { fs: SHAPE_ONLY_FS }) === 'not_absolute' ? undefined : text;
117
+ }
118
+ // checkToolPath against nothing on disk: only its shape rules run (absolute, not a network path).
119
+ const SHAPE_ONLY_FS = Object.freeze({ statSync: () => { throw new Error('shape only'); }, accessSync: () => {}, constants: {} });
120
+
121
+ const TOOL_PATH_PROBLEMS = Object.freeze({
122
+ not_absolute: 'must be an absolute path (it is never looked up on PATH)',
123
+ network: 'must be a path on this computer, not on a network share',
124
+ missing: 'names a file that does not exist',
125
+ not_file: 'must name a regular file',
126
+ not_executable: process.platform === 'win32' ? 'must name a program (.exe)' : 'names a file that is not executable (chmod +x it)',
127
+ unreadable: 'names a file this account cannot read',
128
+ empty: 'names an empty file',
129
+ });
130
+
84
131
  /** A reply label: printable, one line, 1-24 characters; '' → null (the mode's default label). */
85
132
  function cleanLabel(value) {
86
133
  if (value === null || value === undefined) return null;
@@ -102,6 +149,8 @@ const CHECKS = {
102
149
  activityText: (v) => typeof v === 'boolean',
103
150
  strictWorkerApprovals: (v) => typeof v === 'boolean',
104
151
  computerUse: (v) => typeof v === 'boolean',
152
+ transcriptionEnabled: (v) => typeof v === 'boolean',
153
+ transcriptionLanguage: (v) => typeof v === 'string' && TRANSCRIPTION_LANGUAGE_RE.test(v),
105
154
  };
106
155
  const MESSAGES = {
107
156
  enabled: 'enabled must be true or false',
@@ -116,10 +165,21 @@ const MESSAGES = {
116
165
  strictWorkerApprovals: 'strictWorkerApprovals must be true or false',
117
166
  computerUse: 'computerUse must be true or false',
118
167
  brain: 'brain must be null (same as the Assistant) or { provider, model, effort } with a provider of claude-code, codex or opencode',
168
+ transcriptionEnabled: 'transcriptionEnabled must be true or false',
169
+ transcriptionLanguage: "transcriptionLanguage must be 'auto' or a 2-3 letter lowercase language code, such as en or pt",
170
+ transcriptionEnginePath: 'transcriptionEnginePath must be empty (find whisper-cli by itself) or the absolute path of the whisper-cli program',
171
+ transcriptionModelPath: 'transcriptionModelPath must be empty (ggml-small.bin in the transcription folder) or the absolute path of a whisper.cpp model file',
172
+ transcriptionFfmpegPath: 'transcriptionFfmpegPath must be empty (find ffmpeg by itself) or the absolute path of the ffmpeg program',
119
173
  };
120
174
 
121
- /** Validate a user patch (Settings fields only). → the cleaned patch; throws 400 CONFIG_INVALID {field}. */
122
- export function validateConfigPatch(patch = {}) {
175
+ /** Whether a tool path can be used on this computer: null, or why not (transcription.js checkToolPath). */
176
+ const defaultCheckPath = (value, kind) => checkToolPath(value, kind);
177
+
178
+ /**
179
+ * Validate a user patch (Settings fields only). → the cleaned patch; throws 400 CONFIG_INVALID {field}.
180
+ * `checkPath(value, kind)` → null or a problem: how a tool path is checked on disk (tests pass a fake).
181
+ */
182
+ export function validateConfigPatch(patch = {}, { checkPath = defaultCheckPath } = {}) {
123
183
  if (!isPlainObject(patch)) throw configError('config', 'config must be an object');
124
184
  const out = {};
125
185
  for (const [key, value] of Object.entries(patch)) {
@@ -138,6 +198,14 @@ export function validateConfigPatch(patch = {}) {
138
198
  out.brain = brain;
139
199
  continue;
140
200
  }
201
+ if (TOOL_PATH_FIELDS[key]) {
202
+ const toolPath = cleanToolPath(value);
203
+ if (toolPath === undefined) throw configError(key, MESSAGES[key]);
204
+ const problem = toolPath ? checkPath(toolPath, TOOL_PATH_FIELDS[key]) : null;
205
+ if (problem) throw configError(key, `${key} ${TOOL_PATH_PROBLEMS[problem] || 'names a file that cannot be used'}: ${toolPath}`);
206
+ out[key] = toolPath;
207
+ continue;
208
+ }
141
209
  if (!CHECKS[key](value)) throw configError(key, MESSAGES[key]);
142
210
  out[key] = value;
143
211
  }
@@ -157,13 +225,28 @@ function cleanOwner(value) {
157
225
  return { masked, boundAt: toMs(value.boundAt), via: value.via === 'claim' ? 'claim' : 'self_confirm' };
158
226
  }
159
227
 
228
+ /**
229
+ * A level 'autonomous' stored with an `autonomousUntil` (any value but null) was turned on under
230
+ * the 8-hour rule of 2.0.5 and before, whether that window is still open or not: it does not
231
+ * become Autonomous without a time limit. → the stored value as level 'ask' with no
232
+ * autonomousUntil, or null when `stored` is not such a value. Autonomous stored with
233
+ * autonomousUntil null is today's state and stays.
234
+ */
235
+ export function migrateLegacyAutonomous(stored) {
236
+ if (!isPlainObject(stored) || stored.level !== 'autonomous') return null;
237
+ if (stored.autonomousUntil === null || stored.autonomousUntil === undefined) return null;
238
+ return { ...stored, level: 'ask', autonomousUntil: null };
239
+ }
240
+
160
241
  /** Defaults + stored values; a bad stored value falls back to its default, field by field. */
161
242
  export function effectiveConfig(stored = {}) {
162
- const raw = isPlainObject(stored) ? stored : {};
243
+ const raw = isPlainObject(stored) ? migrateLegacyAutonomous(stored) || stored : {};
163
244
  const out = {};
164
245
  for (const key of USER_FIELDS) {
165
246
  if (key === 'replyLabel') { const label = cleanLabel(raw.replyLabel); out.replyLabel = label === undefined ? null : label; continue; }
166
247
  if (key === 'brain') { const brain = cleanBrainChoice(raw.brain); out.brain = brain === undefined ? null : brain; continue; }
248
+ // A stored path keeps its shape only: whether the file still works is the host's check, right before it runs.
249
+ if (TOOL_PATH_FIELDS[key]) { const toolPath = cleanToolPath(raw[key]); out[key] = toolPath === undefined ? '' : toolPath; continue; }
167
250
  out[key] = raw[key] !== undefined && CHECKS[key](raw[key]) ? raw[key] : USER_DEFAULTS[key];
168
251
  }
169
252
  out.mode = MODES.includes(raw.mode) ? raw.mode : SERVICE_DEFAULTS.mode;
@@ -172,7 +255,7 @@ export function effectiveConfig(stored = {}) {
172
255
  ? raw.previousSessionIds.map((id) => (typeof id === 'string' ? id : id?.id)).filter((id) => typeof id === 'string' && id).slice(0, 10)
173
256
  : [];
174
257
  out.owner = cleanOwner(raw.owner);
175
- out.autonomousUntil = toMs(raw.autonomousUntil);
258
+ out.autonomousUntil = null; // legacy: Autonomous has no time limit (migrateLegacyAutonomous)
176
259
  out.paused = raw.paused === true;
177
260
  out.pausedBy = out.paused && (raw.pausedBy === 'desktop' || raw.pausedBy === 'phone') ? raw.pausedBy : null;
178
261
  out.version = Number.isSafeInteger(raw.version) && raw.version >= 0 ? raw.version : 0;
@@ -181,18 +264,23 @@ export function effectiveConfig(stored = {}) {
181
264
  return out;
182
265
  }
183
266
 
184
- /** 'autonomous' only while its window is open; an expired window reads as 'ask'. */
185
- export function currentLevel(config, now = Date.now()) {
186
- if (config?.level !== 'autonomous') return LEVELS.includes(config?.level) ? config.level : 'ask';
187
- return toMs(config.autonomousUntil) > now ? 'autonomous' : 'ask';
267
+ /**
268
+ * The configured level ('ask' when it is not a known one). Autonomous has no time limit: it
269
+ * holds until the owner lowers it. A legacy 8-hour Autonomous (migrateLegacyAutonomous) is 'ask'.
270
+ */
271
+ export function currentLevel(config) {
272
+ if (migrateLegacyAutonomous(config)) return 'ask';
273
+ return LEVELS.includes(config?.level) ? config.level : 'ask';
188
274
  }
189
275
 
190
276
  /**
191
277
  * @param {object} deps
192
278
  * @param {(key:string)=>string|null} deps.get getKvConfig
193
279
  * @param {(key:string, value:string)=>void} deps.set setKvConfig
280
+ * @param {() => void} [deps.onLegacyAutonomous] a legacy 8-hour Autonomous was stored as Ask (once, after it was saved)
281
+ * @param {(value:string, kind:'program'|'file') => string|null} [deps.checkPath] how update() checks a tool path on disk
194
282
  */
195
- export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.now, log = () => {} } = {}) {
283
+ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.now, log = () => {}, onLegacyAutonomous = null, checkPath = defaultCheckPath } = {}) {
196
284
  let cache = null; // { at, value }
197
285
 
198
286
  function readStored() {
@@ -202,12 +290,34 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
202
290
  if (!raw) return {};
203
291
  try {
204
292
  const value = JSON.parse(raw);
205
- return isPlainObject(value) ? value : {};
293
+ return isPlainObject(value) ? migrateStored(value) : {};
206
294
  } catch (error) {
207
295
  log('whatsapp:config-corrupt', error?.message || String(error));
208
296
  return {};
209
297
  }
210
298
  }
299
+ /**
300
+ * A legacy 8-hour Autonomous (migrateLegacyAutonomous) found by any read is stored as Ask at
301
+ * once, with the version bumped (an open Settings tab reloads before it saves), and reported
302
+ * once. A store that cannot be written still reads it as Ask, and tries again at the next read.
303
+ */
304
+ function migrateStored(value) {
305
+ const migrated = migrateLegacyAutonomous(value);
306
+ if (!migrated) return value;
307
+ const version = Number.isSafeInteger(value.version) && value.version >= 0 ? value.version : 0;
308
+ const next = { ...migrated, version: version + 1 };
309
+ try {
310
+ if (typeof set !== 'function') return next;
311
+ set(WHATSAPP_CONFIG_KEY, JSON.stringify(next));
312
+ } catch (error) {
313
+ log('whatsapp:config-unwritable', `the earlier Autonomous could not be stored as Ask: ${error?.message || String(error)}`);
314
+ return next;
315
+ }
316
+ cache = null;
317
+ log('whatsapp:config-migrated', 'Autonomous from an earlier version (8-hour window) ended; the level is Ask');
318
+ try { onLegacyAutonomous?.(); } catch {}
319
+ return next;
320
+ }
211
321
  function persist(value) {
212
322
  if (typeof set === 'function') set(WHATSAPP_CONFIG_KEY, JSON.stringify(value));
213
323
  cache = null;
@@ -221,8 +331,8 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
221
331
 
222
332
  /**
223
333
  * Service-owned write: service fields, internal state and user fields the
224
- * service decides (the confirmed autonomous level, its expiry). A change to
225
- * a user field bumps the version so an open Settings tab reloads first.
334
+ * service decides (the confirmed autonomous level). A change to a user field
335
+ * bumps the version so an open Settings tab reloads first.
226
336
  */
227
337
  function write(patch = {}) {
228
338
  if (!isPlainObject(patch)) return read();
@@ -239,6 +349,10 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
239
349
  if (key === 'version') continue;
240
350
  // Computer use is turned on by the person at this computer only (update()); the service may only turn it off.
241
351
  if (key === 'computerUse' && value !== false) { log('whatsapp:config-ignored', 'computerUse is turned on in Settings only'); continue; }
352
+ // What the host runs (whisper-cli, FFmpeg) and the model it loads are chosen in Settings only; a service write may clear them.
353
+ if (TOOL_PATH_FIELDS[key] && value !== '' && value !== null) { log('whatsapp:config-ignored', `${key} is set in Settings only`); continue; }
354
+ // A legacy field: Autonomous has no time limit, so nothing writes an expiry (only null, which clears it).
355
+ if (key === 'autonomousUntil' && value !== null) { log('whatsapp:config-ignored', 'autonomousUntil is a legacy field: Autonomous has no time limit'); continue; }
242
356
  next[key] = value;
243
357
  if (USER_FIELDS.includes(key) && JSON.stringify(effectiveConfig(next)[key]) !== JSON.stringify(before[key])) userChanged = true;
244
358
  }
@@ -253,7 +367,7 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
253
367
  * level waits for the phone (the service holds the escalation).
254
368
  */
255
369
  function update(patch = {}, { expectedVersion = null, confirmEscalation = false } = {}) {
256
- const clean = validateConfigPatch(patch);
370
+ const clean = validateConfigPatch(patch, { checkPath });
257
371
  const stored = readStored();
258
372
  const current = effectiveConfig(stored);
259
373
  if (expectedVersion !== null && expectedVersion !== undefined && Number(expectedVersion) !== current.version) {
@@ -263,7 +377,7 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
263
377
  }
264
378
  let pending = false;
265
379
  if (clean.level !== undefined) {
266
- const effective = currentLevel(current, now());
380
+ const effective = currentLevel(current);
267
381
  if (clean.level === 'autonomous') {
268
382
  if (effective !== 'autonomous') {
269
383
  if (confirmEscalation !== true) throw configError('level', 'Autonomous needs a second confirmation on this computer, then an ALLOW code from your phone.', { code: 'CONFIRM_REQUIRED' });
@@ -271,7 +385,7 @@ export function createWhatsAppConfigStore({ get, set, ttlMs = 3000, now = Date.n
271
385
  }
272
386
  delete clean.level;
273
387
  } else if (RANK[clean.level] < RANK[effective] || clean.level !== current.level) {
274
- // Lowering (or leaving an expired autonomous) applies at once and closes the window.
388
+ // Lowering applies at once (and clears the legacy autonomousUntil).
275
389
  clean.autonomousUntil = null;
276
390
  }
277
391
  }