synabun 2.0.5 → 2.0.9

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 (527) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/post-plan.mjs +6 -30
  6. package/hooks/claude-code/session-start.mjs +1 -4
  7. package/hooks/claude-code/shared.mjs +112 -75
  8. package/lib/child-path.js +303 -0
  9. package/lib/claude-hooks.js +2 -1
  10. package/lib/data-home-migration.js +2 -0
  11. package/lib/desktop-app.js +265 -0
  12. package/lib/external-tools.js +7 -3
  13. package/lib/log-redact.js +102 -0
  14. package/lib/package-processes.js +670 -0
  15. package/lib/packaged-runtime.js +138 -11
  16. package/lib/runtime-dependencies.js +225 -0
  17. package/mcp-server/dist/http.d.ts +3 -1
  18. package/mcp-server/dist/http.d.ts.map +1 -1
  19. package/mcp-server/dist/http.js +15 -4
  20. package/mcp-server/dist/http.js.map +1 -1
  21. package/mcp-server/dist/index.d.ts.map +1 -1
  22. package/mcp-server/dist/index.js +9 -6
  23. package/mcp-server/dist/index.js.map +1 -1
  24. package/mcp-server/dist/services/browser-bench.d.ts +1 -1
  25. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
  26. package/mcp-server/dist/services/browser-bench.js +1 -1
  27. package/mcp-server/dist/services/browser-bench.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/categories.d.ts +35 -2
  33. package/mcp-server/dist/services/categories.d.ts.map +1 -1
  34. package/mcp-server/dist/services/categories.js +77 -33
  35. package/mcp-server/dist/services/categories.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
  41. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
  42. package/mcp-server/dist/services/desktop-bench.js +1 -1
  43. package/mcp-server/dist/services/desktop-bench.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +11 -0
  45. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  46. package/mcp-server/dist/services/identity.js +24 -0
  47. package/mcp-server/dist/services/identity.js.map +1 -1
  48. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  49. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  50. package/mcp-server/dist/services/neural-interface.js +34 -2
  51. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  52. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  53. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  54. package/mcp-server/dist/services/typesafe-config.js +4 -1
  55. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  56. package/mcp-server/dist/tools/agents.d.ts +11 -8
  57. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  58. package/mcp-server/dist/tools/agents.js +35 -14
  59. package/mcp-server/dist/tools/agents.js.map +1 -1
  60. package/mcp-server/dist/tools/bluesky.js +1 -1
  61. package/mcp-server/dist/tools/bluesky.js.map +1 -1
  62. package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
  63. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  64. package/mcp-server/dist/tools/browser-advanced.js +1 -1
  65. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  66. package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
  67. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
  68. package/mcp-server/dist/tools/browser-batch.js +1 -1
  69. package/mcp-server/dist/tools/browser-batch.js.map +1 -1
  70. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
  71. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
  72. package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
  73. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
  74. package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
  75. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  76. package/mcp-server/dist/tools/browser-interact.js +1 -1
  77. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  78. package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
  79. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  80. package/mcp-server/dist/tools/browser-navigate.js +1 -1
  81. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  82. package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
  83. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  84. package/mcp-server/dist/tools/browser-observe.js +1 -1
  85. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  86. package/mcp-server/dist/tools/card-tools.d.ts +1 -1
  87. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
  88. package/mcp-server/dist/tools/card-tools.js +1 -1
  89. package/mcp-server/dist/tools/card-tools.js.map +1 -1
  90. package/mcp-server/dist/tools/category.d.ts +1 -1
  91. package/mcp-server/dist/tools/category.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/category.js +5 -1
  93. package/mcp-server/dist/tools/category.js.map +1 -1
  94. package/mcp-server/dist/tools/choice.d.ts +1 -1
  95. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/choice.js +1 -1
  97. package/mcp-server/dist/tools/choice.js.map +1 -1
  98. package/mcp-server/dist/tools/computer.d.ts +1 -1
  99. package/mcp-server/dist/tools/computer.d.ts.map +1 -1
  100. package/mcp-server/dist/tools/computer.js +1 -1
  101. package/mcp-server/dist/tools/computer.js.map +1 -1
  102. package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
  103. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
  104. package/mcp-server/dist/tools/discord-channel.js +1 -1
  105. package/mcp-server/dist/tools/discord-channel.js.map +1 -1
  106. package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
  107. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
  108. package/mcp-server/dist/tools/discord-guild.js +1 -1
  109. package/mcp-server/dist/tools/discord-guild.js.map +1 -1
  110. package/mcp-server/dist/tools/discord-member.d.ts +1 -1
  111. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
  112. package/mcp-server/dist/tools/discord-member.js +1 -1
  113. package/mcp-server/dist/tools/discord-member.js.map +1 -1
  114. package/mcp-server/dist/tools/discord-message.d.ts +1 -1
  115. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
  116. package/mcp-server/dist/tools/discord-message.js +1 -1
  117. package/mcp-server/dist/tools/discord-message.js.map +1 -1
  118. package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
  119. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
  120. package/mcp-server/dist/tools/discord-onboarding.js +1 -1
  121. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
  122. package/mcp-server/dist/tools/discord-role.d.ts +1 -1
  123. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
  124. package/mcp-server/dist/tools/discord-role.js +1 -1
  125. package/mcp-server/dist/tools/discord-role.js.map +1 -1
  126. package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
  127. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
  128. package/mcp-server/dist/tools/discord-thread.js +1 -1
  129. package/mcp-server/dist/tools/discord-thread.js.map +1 -1
  130. package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
  131. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
  132. package/mcp-server/dist/tools/discord-webhook.js +1 -1
  133. package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
  134. package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
  135. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
  136. package/mcp-server/dist/tools/fb-groups.js +1 -1
  137. package/mcp-server/dist/tools/fb-groups.js.map +1 -1
  138. package/mcp-server/dist/tools/forget.d.ts +1 -1
  139. package/mcp-server/dist/tools/forget.d.ts.map +1 -1
  140. package/mcp-server/dist/tools/forget.js +1 -1
  141. package/mcp-server/dist/tools/forget.js.map +1 -1
  142. package/mcp-server/dist/tools/git-tools.d.ts +1 -1
  143. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
  144. package/mcp-server/dist/tools/git-tools.js +1 -1
  145. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  146. package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
  147. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
  148. package/mcp-server/dist/tools/gsc/experience.js +1 -1
  149. package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
  150. package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
  151. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
  152. package/mcp-server/dist/tools/gsc/indexing.js +1 -1
  153. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
  154. package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
  155. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
  156. package/mcp-server/dist/tools/gsc/inspect.js +1 -1
  157. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
  158. package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
  159. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
  160. package/mcp-server/dist/tools/gsc/navigate.js +1 -1
  161. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
  162. package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
  163. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
  164. package/mcp-server/dist/tools/gsc/performance.js +1 -1
  165. package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
  166. package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
  167. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
  168. package/mcp-server/dist/tools/gsc/settings.js +1 -1
  169. package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
  170. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  171. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
  172. package/mcp-server/dist/tools/image-tools.js +1 -1
  173. package/mcp-server/dist/tools/image-tools.js.map +1 -1
  174. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
  175. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  176. package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
  177. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  178. package/mcp-server/dist/tools/loop.d.ts +1 -1
  179. package/mcp-server/dist/tools/loop.d.ts.map +1 -1
  180. package/mcp-server/dist/tools/loop.js +1 -1
  181. package/mcp-server/dist/tools/loop.js.map +1 -1
  182. package/mcp-server/dist/tools/memories.d.ts +1 -1
  183. package/mcp-server/dist/tools/memories.d.ts.map +1 -1
  184. package/mcp-server/dist/tools/memories.js +1 -1
  185. package/mcp-server/dist/tools/memories.js.map +1 -1
  186. package/mcp-server/dist/tools/morelogin.d.ts +1 -1
  187. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  188. package/mcp-server/dist/tools/morelogin.js +1 -1
  189. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  190. package/mcp-server/dist/tools/profile.d.ts +1 -1
  191. package/mcp-server/dist/tools/profile.d.ts.map +1 -1
  192. package/mcp-server/dist/tools/profile.js +1 -1
  193. package/mcp-server/dist/tools/profile.js.map +1 -1
  194. package/mcp-server/dist/tools/recall.d.ts +1 -1
  195. package/mcp-server/dist/tools/recall.d.ts.map +1 -1
  196. package/mcp-server/dist/tools/recall.js +1 -1
  197. package/mcp-server/dist/tools/recall.js.map +1 -1
  198. package/mcp-server/dist/tools/reflect.d.ts +1 -1
  199. package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
  200. package/mcp-server/dist/tools/reflect.js +1 -1
  201. package/mcp-server/dist/tools/reflect.js.map +1 -1
  202. package/mcp-server/dist/tools/remember.d.ts +1 -1
  203. package/mcp-server/dist/tools/remember.d.ts.map +1 -1
  204. package/mcp-server/dist/tools/remember.js +13 -11
  205. package/mcp-server/dist/tools/remember.js.map +1 -1
  206. package/mcp-server/dist/tools/restore.d.ts +1 -1
  207. package/mcp-server/dist/tools/restore.d.ts.map +1 -1
  208. package/mcp-server/dist/tools/restore.js +1 -1
  209. package/mcp-server/dist/tools/restore.js.map +1 -1
  210. package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
  211. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
  212. package/mcp-server/dist/tools/style-guide-tools.js +1 -1
  213. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
  214. package/mcp-server/dist/tools/sync.d.ts +1 -1
  215. package/mcp-server/dist/tools/sync.d.ts.map +1 -1
  216. package/mcp-server/dist/tools/sync.js +1 -1
  217. package/mcp-server/dist/tools/sync.js.map +1 -1
  218. package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
  219. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
  220. package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
  221. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
  222. package/mcp-server/dist/tools/utils.d.ts +1 -1
  223. package/mcp-server/dist/tools/utils.d.ts.map +1 -1
  224. package/mcp-server/dist/tools/utils.js +1 -1
  225. package/mcp-server/dist/tools/utils.js.map +1 -1
  226. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  227. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  228. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  229. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  230. package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
  231. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  232. package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
  233. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  234. package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
  235. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
  236. package/mcp-server/dist/tools/youtube/analytics.js +1 -1
  237. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
  238. package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
  239. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
  240. package/mcp-server/dist/tools/youtube/download.js +1 -1
  241. package/mcp-server/dist/tools/youtube/download.js.map +1 -1
  242. package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
  243. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
  244. package/mcp-server/dist/tools/youtube/media.js +1 -1
  245. package/mcp-server/dist/tools/youtube/media.js.map +1 -1
  246. package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
  247. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
  248. package/mcp-server/dist/tools/youtube/metadata.js +1 -1
  249. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
  250. package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
  251. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
  252. package/mcp-server/dist/tools/youtube/navigate.js +1 -1
  253. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
  254. package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
  255. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
  256. package/mcp-server/dist/tools/youtube/playlist.js +1 -1
  257. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
  258. package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
  259. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
  260. package/mcp-server/dist/tools/youtube/schedule.js +1 -1
  261. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
  262. package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
  263. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
  264. package/mcp-server/dist/tools/youtube/source.js +1 -1
  265. package/mcp-server/dist/tools/youtube/source.js.map +1 -1
  266. package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
  267. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
  268. package/mcp-server/dist/tools/youtube/upload.js +4 -9
  269. package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
  270. package/mcp-server/dist/tui.js +18 -5
  271. package/mcp-server/dist/tui.js.map +1 -1
  272. package/mcp-server/dist/version.d.ts +5 -0
  273. package/mcp-server/dist/version.d.ts.map +1 -0
  274. package/mcp-server/dist/version.js +21 -0
  275. package/mcp-server/dist/version.js.map +1 -0
  276. package/mcp-server/package-lock.json +3 -3
  277. package/mcp-server/package.json +3 -3
  278. package/mcp-server/run.mjs +15 -9
  279. package/mcp-server/src/http.ts +18 -3
  280. package/mcp-server/src/index.ts +9 -6
  281. package/mcp-server/src/services/browser-bench.ts +1 -1
  282. package/mcp-server/src/services/budget-judgments.ts +183 -0
  283. package/mcp-server/src/services/categories.ts +77 -32
  284. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  285. package/mcp-server/src/services/desktop-bench.ts +1 -1
  286. package/mcp-server/src/services/identity.ts +27 -0
  287. package/mcp-server/src/services/neural-interface.ts +34 -2
  288. package/mcp-server/src/services/typesafe-config.ts +4 -1
  289. package/mcp-server/src/tools/agents.ts +34 -15
  290. package/mcp-server/src/tools/bluesky.ts +1 -1
  291. package/mcp-server/src/tools/browser-advanced.ts +1 -1
  292. package/mcp-server/src/tools/browser-batch.ts +1 -1
  293. package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
  294. package/mcp-server/src/tools/browser-interact.ts +1 -1
  295. package/mcp-server/src/tools/browser-navigate.ts +1 -1
  296. package/mcp-server/src/tools/browser-observe.ts +1 -1
  297. package/mcp-server/src/tools/card-tools.ts +1 -1
  298. package/mcp-server/src/tools/category.ts +6 -1
  299. package/mcp-server/src/tools/choice.ts +1 -1
  300. package/mcp-server/src/tools/computer.ts +1 -1
  301. package/mcp-server/src/tools/discord-channel.ts +1 -1
  302. package/mcp-server/src/tools/discord-guild.ts +1 -1
  303. package/mcp-server/src/tools/discord-member.ts +1 -1
  304. package/mcp-server/src/tools/discord-message.ts +1 -1
  305. package/mcp-server/src/tools/discord-onboarding.ts +1 -1
  306. package/mcp-server/src/tools/discord-role.ts +1 -1
  307. package/mcp-server/src/tools/discord-thread.ts +1 -1
  308. package/mcp-server/src/tools/discord-webhook.ts +1 -1
  309. package/mcp-server/src/tools/fb-groups.ts +1 -1
  310. package/mcp-server/src/tools/forget.ts +1 -1
  311. package/mcp-server/src/tools/git-tools.ts +1 -1
  312. package/mcp-server/src/tools/gsc/experience.ts +1 -1
  313. package/mcp-server/src/tools/gsc/indexing.ts +1 -1
  314. package/mcp-server/src/tools/gsc/inspect.ts +1 -1
  315. package/mcp-server/src/tools/gsc/navigate.ts +1 -1
  316. package/mcp-server/src/tools/gsc/performance.ts +1 -1
  317. package/mcp-server/src/tools/gsc/settings.ts +1 -1
  318. package/mcp-server/src/tools/image-tools.ts +1 -1
  319. package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
  320. package/mcp-server/src/tools/loop.ts +1 -1
  321. package/mcp-server/src/tools/memories.ts +1 -1
  322. package/mcp-server/src/tools/morelogin.ts +1 -1
  323. package/mcp-server/src/tools/profile.ts +1 -1
  324. package/mcp-server/src/tools/recall.ts +1 -1
  325. package/mcp-server/src/tools/reflect.ts +1 -1
  326. package/mcp-server/src/tools/remember.ts +13 -12
  327. package/mcp-server/src/tools/restore.ts +1 -1
  328. package/mcp-server/src/tools/style-guide-tools.ts +1 -1
  329. package/mcp-server/src/tools/sync.ts +1 -1
  330. package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
  331. package/mcp-server/src/tools/utils.ts +1 -1
  332. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  333. package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
  334. package/mcp-server/src/tools/youtube/analytics.ts +1 -1
  335. package/mcp-server/src/tools/youtube/download.ts +1 -1
  336. package/mcp-server/src/tools/youtube/media.ts +1 -1
  337. package/mcp-server/src/tools/youtube/metadata.ts +1 -1
  338. package/mcp-server/src/tools/youtube/navigate.ts +1 -1
  339. package/mcp-server/src/tools/youtube/playlist.ts +1 -1
  340. package/mcp-server/src/tools/youtube/schedule.ts +1 -1
  341. package/mcp-server/src/tools/youtube/source.ts +1 -1
  342. package/mcp-server/src/tools/youtube/upload.ts +4 -12
  343. package/mcp-server/src/tui.ts +24 -10
  344. package/mcp-server/src/version.ts +22 -0
  345. package/neural-interface/i18n/en.json +645 -57
  346. package/neural-interface/i18n/pt-BR.json +644 -56
  347. package/neural-interface/lib/account-usage.js +163 -0
  348. package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
  349. package/neural-interface/lib/agent-sandbox-setting.js +57 -0
  350. package/neural-interface/lib/assistant-api.js +274 -52
  351. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  352. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  353. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  354. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  355. package/neural-interface/lib/assistant-budget-split.js +408 -0
  356. package/neural-interface/lib/assistant-budget.js +94 -95
  357. package/neural-interface/lib/assistant-config.js +62 -14
  358. package/neural-interface/lib/assistant-dispatch.js +432 -125
  359. package/neural-interface/lib/assistant-jev.js +26 -3
  360. package/neural-interface/lib/assistant-persona.js +68 -11
  361. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  362. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  363. package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
  364. package/neural-interface/lib/assistant-route-gate.js +19 -3
  365. package/neural-interface/lib/assistant-router.js +373 -42
  366. package/neural-interface/lib/assistant-runtime.js +312 -48
  367. package/neural-interface/lib/assistant-user-writer.js +4 -2
  368. package/neural-interface/lib/augmented-path.js +317 -53
  369. package/neural-interface/lib/claude-accounts.js +342 -31
  370. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  371. package/neural-interface/lib/claude-executable.js +6 -1
  372. package/neural-interface/lib/claude-model-catalog.js +3 -3
  373. package/neural-interface/lib/claude-panel-auth.js +221 -0
  374. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  375. package/neural-interface/lib/cli-install-api.js +137 -0
  376. package/neural-interface/lib/cli-install.js +797 -0
  377. package/neural-interface/lib/cli-path.js +130 -0
  378. package/neural-interface/lib/cli-resolver.js +744 -0
  379. package/neural-interface/lib/cli-update-planner.js +143 -7
  380. package/neural-interface/lib/codex-account-auth.js +180 -0
  381. package/neural-interface/lib/codex-accounts.js +502 -0
  382. package/neural-interface/lib/codex-capabilities.js +2 -1
  383. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  384. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  385. package/neural-interface/lib/codex-panel-auth.js +62 -0
  386. package/neural-interface/lib/codex-session-settings.js +1 -1
  387. package/neural-interface/lib/codex-settings-schema.js +122 -0
  388. package/neural-interface/lib/desktop/config.js +3 -0
  389. package/neural-interface/lib/exec-loop-command.js +3 -1
  390. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  391. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  392. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  393. package/neural-interface/lib/git-bash/install.js +529 -0
  394. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  395. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  396. package/neural-interface/lib/git-bash/manager.js +268 -0
  397. package/neural-interface/lib/git-bash/pins.js +206 -0
  398. package/neural-interface/lib/git-bash/pins.json +34 -0
  399. package/neural-interface/lib/git-bash/resolver.js +194 -0
  400. package/neural-interface/lib/git-read.js +34 -2
  401. package/neural-interface/lib/install-details.js +163 -0
  402. package/neural-interface/lib/mcp-installer.js +14 -2
  403. package/neural-interface/lib/native-loop-providers.js +22 -4
  404. package/neural-interface/lib/native-loop-runtime.js +9 -4
  405. package/neural-interface/lib/os-path.js +319 -0
  406. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  407. package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
  408. package/neural-interface/lib/pty-host/core.js +2 -1
  409. package/neural-interface/lib/pty-host/manager.js +27 -2
  410. package/neural-interface/lib/remote-policy.js +20 -11
  411. package/neural-interface/lib/session-indexer.js +3 -1
  412. package/neural-interface/lib/session-title-generator.js +6 -3
  413. package/neural-interface/lib/system-about.js +340 -0
  414. package/neural-interface/lib/system-activity.js +66 -0
  415. package/neural-interface/lib/terminal-env.js +11 -3
  416. package/neural-interface/lib/terminal-path.js +55 -0
  417. package/neural-interface/lib/virtual-socket.js +12 -1
  418. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  419. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  420. package/neural-interface/lib/whatsapp/commands.js +13 -14
  421. package/neural-interface/lib/whatsapp/config.js +134 -20
  422. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  423. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  424. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  425. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  426. package/neural-interface/lib/whatsapp/manager.js +92 -32
  427. package/neural-interface/lib/whatsapp/paths.js +11 -1
  428. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  429. package/neural-interface/lib/whatsapp/redact.js +63 -0
  430. package/neural-interface/lib/whatsapp/service.js +343 -91
  431. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  432. package/neural-interface/package-lock.json +2 -2
  433. package/neural-interface/package.json +1 -1
  434. package/neural-interface/public/index.html +2 -0
  435. package/neural-interface/public/index2d.html +2 -0
  436. package/neural-interface/public/offline.html +16 -1
  437. package/neural-interface/public/onboarding.html +96 -2
  438. package/neural-interface/public/shared/account-i18n.js +7 -0
  439. package/neural-interface/public/shared/account-menu.js +542 -0
  440. package/neural-interface/public/shared/account-usage.js +79 -0
  441. package/neural-interface/public/shared/account-view.js +336 -0
  442. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  443. package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
  444. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  445. package/neural-interface/public/shared/assistant/asst-api.js +39 -2
  446. package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
  447. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  448. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  449. package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
  450. package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
  451. package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
  452. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  453. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  454. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  455. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  456. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  457. package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
  458. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  459. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  460. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  461. package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
  462. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  463. package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
  464. package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
  465. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  466. package/neural-interface/public/shared/claude-account-view.js +3 -0
  467. package/neural-interface/public/shared/cli-install-view.js +392 -0
  468. package/neural-interface/public/shared/cli-status.js +282 -22
  469. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  470. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  471. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  472. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  473. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  474. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  475. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  476. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  477. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  478. package/neural-interface/public/shared/git-bash-view.js +145 -0
  479. package/neural-interface/public/shared/html-shell.js +1 -0
  480. package/neural-interface/public/shared/i18n.js +19 -1
  481. package/neural-interface/public/shared/install-details-view.js +86 -0
  482. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
  483. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
  484. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  485. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
  486. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  487. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  488. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  489. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  490. package/neural-interface/public/shared/showcase-privacy.js +490 -0
  491. package/neural-interface/public/shared/storage.js +30 -3
  492. package/neural-interface/public/shared/styles.css +281 -32
  493. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  494. package/neural-interface/public/shared/ui-claude-panel.js +262 -217
  495. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  496. package/neural-interface/public/shared/ui-explorer.js +2 -0
  497. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  498. package/neural-interface/public/shared/ui-judgments.js +10 -1
  499. package/neural-interface/public/shared/ui-loading.js +30 -6
  500. package/neural-interface/public/shared/ui-menubar.js +13 -3
  501. package/neural-interface/public/shared/ui-navbar.js +3 -1
  502. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  503. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  504. package/neural-interface/public/shared/ui-sessions.js +9 -3
  505. package/neural-interface/public/shared/ui-settings.js +55 -3
  506. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  507. package/neural-interface/public/shared/ui-sync.js +8 -0
  508. package/neural-interface/public/shared/ui-terminal.js +17 -0
  509. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  510. package/neural-interface/public/shared/ui-update.js +598 -7
  511. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  512. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  513. package/neural-interface/public/shared/worker-end.js +2 -0
  514. package/neural-interface/public/sw.js +1 -1
  515. package/neural-interface/public/variant/2d/main.js +10 -1
  516. package/neural-interface/public/variant/3d/main.js +10 -1
  517. package/neural-interface/scripts/rebuild-pty.js +14 -2
  518. package/neural-interface/server.js +1384 -679
  519. package/npm-shrinkwrap.json +4424 -0
  520. package/package.json +57 -2
  521. package/postinstall.js +61 -47
  522. package/preuninstall.js +8 -35
  523. package/setup.js +80 -47
  524. package/skills/synabun/modules/idea.md +1 -1
  525. package/skills/synabun/modules/schedule.md +1 -1
  526. package/updater.cmd +2 -2
  527. package/updater.mjs +97 -3
@@ -12,21 +12,26 @@ import { DispatchError } from './assistant-dispatch.js';
12
12
  import { openServable } from './assistant-media.js';
13
13
  import { designFit, targetMakes } from './assistant-router.js';
14
14
  import { normalizeEffort } from './effort-levels.js';
15
- import { BUDGET_DEFAULTS, BUDGET_FIELDS, BUDGET_MAX_USD, BUDGET_MIN_USD } from './assistant-budget.js';
15
+ import { BUDGET_DEFAULT_USD, BUDGET_MAX_USD, BUDGET_MIN_USD, BUDGET_WARN_RATIO } from './assistant-budget.js';
16
16
  import { tooLargeMessage } from './assistant-attachments.js';
17
17
  import { getSessionPolicy, requiresHumanApproval } from './remote-policy.js';
18
18
  import { clampPlanDispatch, PLAN_DISPATCH_NOTE, runCanChangeCode } from './assistant-plan-permissions.js';
19
+ import { noStore, requireJson, requireLocal, requireLocalHost, sendGuardError } from './http-guards.js';
20
+
21
+ /** The header whatsapp_send_file's caller proves it is the WhatsApp conversation's own brain with (assistant-runtime.js BRAIN_CREDENTIAL_RE). */
22
+ export const BRAIN_CREDENTIAL_HEADER = 'x-synabun-brain-token';
23
+ const SEND_FILE_FIELDS = Object.freeze(['path', 'caption', 'as']);
19
24
 
20
25
  /**
21
- * How far each provider's spend can be held to a cap (shown in the Budget tab).
22
- * A cap is checked where the provider lets it be checked; between two checks a
23
- * run can go past it by that much.
26
+ * How far each provider's spend can be held to the budget (the Budget modal shows it). Spend is
27
+ * checked where the provider lets it be checked; between two checks a run can go past its share by
28
+ * that much.
24
29
  */
25
30
  export const BUDGET_ENFORCEMENT = Object.freeze({
26
- 'claude-code': { metered: 'reported', perRun: 'The Claude CLI stops the run (--max-budget-usd); it checks between model calls, so one call can pass the cap.', session: 'Checked after every turn; at the hard cap the run is stopped mid-turn.' },
27
- opencode: { metered: 'reported', perRun: 'OpenCode reports each step\'s cost; the run stops at the first step past its cap.', session: 'Checked at every step.' },
28
- codex: { metered: 'estimated', perRun: 'Codex reports tokens only when a turn ends: priced at the models.dev list price (an API-price estimate; ChatGPT-plan accounts are billed by plan quota) and checked after each turn, so one turn can pass the cap. A failed turn is priced from its rollout.', session: 'Checked after every turn.' },
29
- brain: { metered: 'reported', perRun: 'Checked before every brain turn; a Claude brain\'s CLI also stops at what is left of the brain cap. A Codex brain\'s tokens are priced at list price (estimated); a brain model with no price is refused.', session: 'The brain\'s spend counts toward the session hard cap.' },
31
+ 'claude-code': { metered: 'reported', share: 'Checked between model calls (each call at list price while the turn runs, the reported cost at its end), so one call can pass the share. The Claude CLI also stops a run at its ceiling, or at what the session had left when it started.', session: 'At the budget every run stops.' },
32
+ opencode: { metered: 'reported', share: 'OpenCode reports each step\'s cost; the share is checked at every step, so one step can pass it.', session: 'At the budget every run stops.' },
33
+ codex: { metered: 'estimated', share: 'Codex reports tokens: priced at list price (an API-price estimate; ChatGPT-plan accounts are billed by plan quota) as its rollout records them, so one record or turn can pass the share. A failed turn is priced from its rollout.', session: 'At the budget every run stops.' },
34
+ brain: { metered: 'reported', share: 'Your Assistant\'s own turns draw from its provider\'s share, which keeps a reserve for them; checked before every brain turn. A Claude brain\'s CLI also stops at what that share had left when it started. A brain model with no price is refused.', session: 'At the budget the brain takes no turn until the budget is raised.' },
30
35
  });
31
36
 
32
37
  const OWNER_ONLY = { error: 'The assistant is available to the owner only', code: 'GUEST_FORBIDDEN' };
@@ -89,9 +94,17 @@ export function slimRunView(run) {
89
94
  * @param {object} deps.dispatcher createAssistantDispatcher()
90
95
  * @param {object} [deps.runtime] createAssistantRuntime() (sessions; optional until wired)
91
96
  * @param {object} [deps.claudeAccounts] createClaudeAccounts()
92
- * @param {object} [deps.codexAccounts] { listForClient(), rename(id,label), remove(id, {inUse}), addStart() }
97
+ * @param {object} [deps.codexAccounts] createCodexAccounts() (lib/codex-accounts.js)
93
98
  * @param {Function} [deps.buildCatalog] async () => catalog object (models/accounts/projects/profiles)
94
99
  * @param {Function} [deps.createLoginTerminal] async ({ env, cwd, profile, accountId }) => { terminalSessionId, profile }
100
+ * @param {Function} [deps.claudeSignIn] async (accountId) => { sessionId, status, error } — Claude Code's own
101
+ * `auth login` in a SynaBun terminal under the account's config directory (lib/provider-panel-terminal-auth.js)
102
+ * @param {Function} [deps.claudeSetMode] async (accountId, mode) => row — what the account's sessions use
103
+ * @param {Function} [deps.claudeAccountInUse] (accountId) => '' | why it cannot be removed now
104
+ * @param {Function} [deps.codexSignIn] async (accountId) => { sessionId, status, error } — `codex login` in a SynaBun
105
+ * terminal under the account's CODEX_HOME (lib/provider-panel-terminal-auth.js)
106
+ * @param {Function} [deps.codexSetMode] async (accountId, mode) => row — what the account's sessions use
107
+ * @param {Function} [deps.codexAccountInUse] (accountId) => '' | why it cannot be removed now
95
108
  * @param {object} [deps.attachments] createAttachmentStore() (POST /attachments)
96
109
  */
97
110
  export function createAssistantApi({
@@ -100,8 +113,15 @@ export function createAssistantApi({
100
113
  usage = null,
101
114
  claudeAccounts = null,
102
115
  codexAccounts = null,
116
+ accountUsage = null,
103
117
  buildCatalog = null,
104
118
  createLoginTerminal = null,
119
+ claudeSignIn = null,
120
+ claudeSetMode = null,
121
+ claudeAccountInUse = null,
122
+ codexSignIn = null,
123
+ codexSetMode = null,
124
+ codexAccountInUse = null,
105
125
  broadcastSync = () => {},
106
126
  isGuestRequest = () => false,
107
127
  log = () => {},
@@ -126,6 +146,12 @@ export function createAssistantApi({
126
146
  // Why the engine did not start on this server (a string or null), for the settings page.
127
147
  userContextError = () => null,
128
148
  defaultProject = () => 'global',
149
+ // whatsapp_send_file: the WhatsApp service ({ sendFile }) or a function returning it (it is built
150
+ // after this router), and the port the server listens on (the Host check of that route).
151
+ whatsapp = null,
152
+ port = 3344,
153
+ // The Admin sandbox setting (lib/agent-sandbox-setting.js): one global value for every agent SynaBun starts.
154
+ adminSandbox = null,
129
155
  } = {}) {
130
156
  if (!dispatcher) throw new Error('createAssistantApi requires a dispatcher');
131
157
  const router = Router();
@@ -275,6 +301,7 @@ export function createAssistantApi({
275
301
  // ── catalog ────────────────────────────────────────────────────────────────
276
302
  router.get('/catalog', async (req, res) => {
277
303
  try {
304
+ await accountUsage?.refresh();
278
305
  // refresh=1 rebuilds from the providers' endpoints; deep=1 also makes them ask the CLI / app-server again.
279
306
  const deep = req.query.deep === '1';
280
307
  const force = req.query.refresh === '1' || deep;
@@ -288,13 +315,21 @@ export function createAssistantApi({
288
315
  let preferences = {};
289
316
  try { preferences = configStore?.routing?.().preferences || {}; } catch {}
290
317
  const brief = await catalog.get({ force, deep, view: 'brain', provider, q, preferences });
291
- return res.json({ ok: true, generatedAt: new Date(now()).toISOString(), ...brief, limits: dispatcher.limits, runs: dispatcher.list({ activeOnly: true }), totals: dispatcher.totals() });
318
+ // Accounts are read live (the catalog build can be seconds old): every Claude and Codex account with
319
+ // its mode, whether it is signed in and its identity, and the account a worker gets when a dispatch
320
+ // names none.
321
+ const accounts = {
322
+ ...(brief.accounts || {}),
323
+ ...(claudeAccounts ? { 'claude-code': accountUsage?.list()?.['claude-code'] || claudeAccounts.listForClient() } : {}),
324
+ ...(codexAccounts ? { codex: accountUsage?.list()?.codex || codexAccounts.listForClient() } : {}),
325
+ };
326
+ return res.json({ ok: true, generatedAt: new Date(now()).toISOString(), ...brief, accounts, claudeWorkerAccount: claudeWorkerAccountId(), codexWorkerAccount: codexWorkerAccountId(), limits: dispatcher.limits, runs: dispatcher.list({ activeOnly: true }), totals: dispatcher.totals() });
292
327
  }
293
328
  // The catalog says how long one of its builds is good for (one without a verified list is redone
294
329
  // within seconds), so it is asked every time: this answer is reused only while it hands back the
295
330
  // build the answer was made from (a rebuild or invalidate() gives another one).
296
331
  const base = typeof buildCatalog === 'function' ? await buildCatalog({ force, deep }) : NO_CATALOG;
297
- if (!force && catalogCache && catalogCache.base === base && now() - catalogCache.at < 30_000) return res.json(catalogCache.value);
332
+ if (!force && catalogCache && catalogCache.base === base && now() - catalogCache.at < 30_000) return res.json(accountUsage ? { ...catalogCache.value, accounts: { ...catalogCache.value.accounts, ...accountUsage.list() } } : catalogCache.value);
298
333
  // Hidden rows stay server-side (view=manage lists them); the lists themselves ship.
299
334
  const { hiddenRows: _hiddenRows, ...visible } = base || {};
300
335
  const value = {
@@ -303,9 +338,11 @@ export function createAssistantApi({
303
338
  ...visible,
304
339
  accounts: {
305
340
  ...(base.accounts || {}),
306
- 'claude-code': claudeAccounts ? claudeAccounts.listForClient() : (base.accounts?.['claude-code'] || []),
307
- codex: codexAccounts ? codexAccounts.listForClient() : (base.accounts?.codex || []),
341
+ 'claude-code': accountUsage?.list()?.['claude-code'] || (claudeAccounts ? claudeAccounts.listForClient() : (base.accounts?.['claude-code'] || [])),
342
+ codex: accountUsage?.list()?.codex || (codexAccounts ? codexAccounts.listForClient() : (base.accounts?.codex || [])),
308
343
  },
344
+ claudeWorkerAccount: claudeWorkerAccountId(),
345
+ codexWorkerAccount: codexWorkerAccountId(),
309
346
  limits: dispatcher.limits,
310
347
  runs: dispatcher.list({ activeOnly: true }),
311
348
  totals: dispatcher.totals(),
@@ -469,16 +506,20 @@ export function createAssistantApi({
469
506
  } catch (error) { sendError(res, error, 400); }
470
507
  });
471
508
  // ── budget ─────────────────────────────────────────────────────────────────
509
+ /** A session's budget view (dispatcher → budget planner): the setting, spend, and the live split. */
510
+ function sessionBudgetView(sessionId) {
511
+ if (!sessionId) return null;
512
+ try { return dispatcher.sessionBudget?.(String(sessionId)) || null; } catch { return null; }
513
+ }
472
514
  function budgetPayload(sessionId = null) {
473
515
  const current = configStore.budget();
474
- const budget = Object.fromEntries(BUDGET_FIELDS.map((field) => [field, current[field]]));
475
- let session = null;
476
- if (sessionId) { try { session = dispatcher.sessionBudget?.(String(sessionId)) || null; } catch { session = null; } }
477
516
  let pricingStatus = null;
478
517
  try { pricingStatus = pricing?.status?.() || null; } catch { pricingStatus = null; }
479
518
  return {
480
- ok: true, version: current.version, budget, defaults: { ...BUDGET_DEFAULTS }, sources: current.sources, repairs: current.repairs,
481
- bounds: { minUsd: BUDGET_MIN_USD, maxUsd: BUDGET_MAX_USD }, session, pricing: pricingStatus, enforcement: BUDGET_ENFORCEMENT,
519
+ ok: true, version: current.version,
520
+ budget: { unlimited: current.unlimited === true, amountUsd: current.amountUsd, version: current.version, updatedAt: current.updatedAt || null, source: current.source || 'saved' },
521
+ defaults: { unlimited: false, amountUsd: BUDGET_DEFAULT_USD }, bounds: { minUsd: BUDGET_MIN_USD, maxUsd: BUDGET_MAX_USD }, warnRatio: BUDGET_WARN_RATIO,
522
+ session: sessionBudgetView(sessionId), pricing: pricingStatus, enforcement: BUDGET_ENFORCEMENT,
482
523
  };
483
524
  }
484
525
  // ── User context (Settings → Automation behavior): the three layers and what a new conversation is told ──
@@ -525,13 +566,16 @@ export function createAssistantApi({
525
566
  if (callerContext(req).kind !== 'ui') return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Budget settings are changed from the assistant panel only.' });
526
567
  try {
527
568
  const body = req.body || {};
528
- const patch = body.budget && typeof body.budget === 'object' ? body.budget : {};
569
+ const patch = body.budget && typeof body.budget === 'object' && !Array.isArray(body.budget) ? body.budget : null;
570
+ if (!patch) return res.status(400).json({ ok: false, code: 'BUDGET_INVALID', error: 'Send { budget: { unlimited?, amountUsd? }, version }', message: 'Send { budget: { unlimited?, amountUsd? }, version }', field: 'budget', version: null });
529
571
  const expectedVersion = body.version ?? null;
530
572
  const saved = configStore.patchBudget(patch, { expectedVersion });
531
- // Applies now: the dispatcher re-reads its limits, the runtime re-checks every session.
573
+ // Applies now: the planner divides every open session's budget again, the runtime re-checks
574
+ // every session (held mailboxes, the brain's next turn) and every panel hears it.
532
575
  try { dispatcher.refreshLimits?.(); } catch {}
533
- try { runtime?.onBudgetChanged?.(); } catch {}
534
- broadcastSync({ type: 'assistant:budget-changed', version: saved.version });
576
+ if (runtime?.onBudgetChanged) { try { runtime.onBudgetChanged(); } catch {} }
577
+ else { try { dispatcher.budget?.onBudgetChanged?.(); } catch {} }
578
+ broadcastSync({ type: 'assistant:budget-changed', version: saved.version, budget: { unlimited: saved.unlimited, amountUsd: saved.amountUsd, version: saved.version, updatedAt: saved.updatedAt || null } });
535
579
  res.json(budgetPayload(body.sessionId || null));
536
580
  } catch (error) {
537
581
  if (error?.code === 'BUDGET_INVALID' || error?.code === 'VERSION_CONFLICT') {
@@ -551,6 +595,45 @@ export function createAssistantApi({
551
595
  try { res.json(runtime.gateCheck({ session: body.session, token: body.token, tool: body.tool, input, inputComplete: body.inputComplete === true, providerSessionId: body.providerSessionId || null, agent: body.agent || null, hello: body.hello === true, host: typeof body.host === 'string' ? body.host : null })); }
552
596
  catch (error) { sendError(res, error); }
553
597
  });
598
+ // ── whatsapp_send_file (docs/whatsapp.md → Sending files) ──────────────────
599
+ // A local file to the owner, for the WhatsApp conversation whose brain is calling. Who calls is the
600
+ // credential that brain alone holds (X-Synabun-Brain-Token: assistant-runtime.js mintBrainCredential),
601
+ // resolved to its session in constant time; never X-Synabun-Role or X-Synabun-Terminal, which any
602
+ // local process can send. A loopback socket and a localhost Host only (the checks of /api/whatsapp);
603
+ // guests were refused above. Only { path, caption?, as? }. Exactly one send, never retried.
604
+ const portOf = () => (typeof port === 'function' ? port() : port);
605
+ let hostGuard = null;
606
+ const localHostOnly = (req, res, next) => {
607
+ const current = portOf();
608
+ if (!hostGuard || hostGuard.port !== current) {
609
+ try { hostGuard = { port: current, check: requireLocalHost(current) }; } catch { return sendGuardError(res, 500, 'BAD_PORT', 'The file route has no valid port.'); }
610
+ }
611
+ return hostGuard.check(req, res, next);
612
+ };
613
+ router.post('/whatsapp/send-file', noStore, requireLocal, localHostOnly, requireJson, async (req, res) => {
614
+ let sessionId = null;
615
+ try { sessionId = runtime?.sessionForBrainCredential?.(req.get(BRAIN_CREDENTIAL_HEADER)) || null; } catch { sessionId = null; }
616
+ if (!sessionId) return sendGuardError(res, 403, 'BRAIN_CREDENTIAL_REQUIRED', 'Only the WhatsApp conversation\'s own Assistant sends files to the phone: nothing was sent.');
617
+ const body = req.body;
618
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return sendGuardError(res, 400, 'BAD_REQUEST', 'Send { path, caption?, as? }.');
619
+ const extra = Object.keys(body).find((key) => !SEND_FILE_FIELDS.includes(key));
620
+ if (extra !== undefined) return sendGuardError(res, 400, 'BAD_REQUEST', `Unknown field "${extra.slice(0, 40)}": a file send takes path, caption and as only.`, extra.slice(0, 40));
621
+ let service = null;
622
+ try { service = typeof whatsapp === 'function' ? whatsapp() : whatsapp; } catch { service = null; }
623
+ if (typeof service?.sendFile !== 'function') return sendGuardError(res, 503, 'WHATSAPP_UNAVAILABLE', 'WhatsApp is not available on this computer: nothing was sent.');
624
+ try {
625
+ res.json(await service.sendFile(sessionId, body));
626
+ } catch (error) {
627
+ const status = Number(error?.status) || 500;
628
+ const out = { ok: false, code: error?.code || 'INTERNAL', error: error?.message || String(error) };
629
+ if (error?.field) out.field = error.field;
630
+ if (error?.uncertain === true) out.uncertain = true;
631
+ if (Number.isFinite(error?.retryAfterMs)) out.retryAfterMs = Math.max(0, Math.round(error.retryAfterMs));
632
+ if (status >= 500) log('assistant:whatsapp-send-file', `${sessionId}: ${out.code}${out.uncertain ? ' (delivery unknown)' : ''}`);
633
+ res.status(status).json(out);
634
+ }
635
+ });
636
+
554
637
  // agent_route: the brain proposes; the router approves, asks, or says pending.
555
638
  router.post('/route', orchestrationGuard, async (req, res) => {
556
639
  if (!requireRouter(res)) return;
@@ -772,13 +855,16 @@ export function createAssistantApi({
772
855
  res.json(brain ? brainWaitView(chat ? chatWaitView(result) : result) : result);
773
856
  } catch (error) { sendError(res, error); }
774
857
  });
775
- router.post('/runs/:runId/send', orchestrationGuard, (req, res) => {
858
+ router.post('/runs/:runId/send', orchestrationGuard, async (req, res) => {
776
859
  try {
777
860
  const body = req.body || {};
778
861
  const assistantSessionId = req.assistantCaller.assistantSessionId || dispatcher.get(req.params.runId)?.assistantSessionId || null;
779
862
  if (planRefusal(req, res, assistantSessionId, 'send a worker a message')) return;
780
863
  if (planState(req, assistantSessionId) === 'plan' && planSendRefusal(res, dispatcher.get(req.params.runId))) return;
781
864
  const origin = body.origin === 'user' ? 'user' : (req.assistantCaller.kind === 'ui' ? (body.origin || 'assistant') : 'assistant');
865
+ // A run whose provider share is used up: the session's unspent budget is divided again first, then
866
+ // sendTurn decides (it refuses with BUDGET_SHARE_EXHAUSTED when no money could move).
867
+ try { await dispatcher.coverFollowUp?.(req.params.runId); } catch { /* sendTurn decides on the shares as they are */ }
782
868
  res.json(dispatcher.sendTurn(req.params.runId, body.text ?? body.message, { origin, queue: body.queue !== false }));
783
869
  } catch (error) { sendError(res, error); }
784
870
  });
@@ -855,6 +941,14 @@ export function createAssistantApi({
855
941
  res.status(201).json({ ok: true, session });
856
942
  } catch (error) { sendError(res, error); }
857
943
  });
944
+ // A session's budget and its live split (the spend chip, the Budget modal): the same view the
945
+ // `assistant:budget` packet carries.
946
+ router.get('/sessions/:id/budget', (req, res) => {
947
+ if (!requireRuntime(res)) return;
948
+ const id = req.params.id;
949
+ if (!runtime.getSession(id, { transcript: false })) return res.status(404).json({ ok: false, code: 'SESSION_NOT_FOUND', error: `Unknown assistant session ${id}` });
950
+ try { res.json({ ok: true, budget: sessionBudgetView(id) }); } catch (error) { sendError(res, error); }
951
+ });
858
952
  router.get('/sessions/:id/usage', (req, res) => {
859
953
  if (!requireRuntime(res)) return;
860
954
  const id = req.params.id;
@@ -870,9 +964,10 @@ export function createAssistantApi({
870
964
  const session = usage?.sessionView(id) ?? null;
871
965
  // Whether work is in progress is the runtime's to say (a turn that has no tokens yet).
872
966
  const head = session ? runtime.usageView?.(id, 'current')?.session || null : null;
873
- return res.json({ ok: true, session: session && head ? { ...session, live: head.live, fidelity: head.fidelity } : session, ...(run ? { run: usage?.runView?.(id, run) ?? null } : {}) });
967
+ return res.json({ ok: true, session: session && head ? { ...session, live: head.live, fidelity: head.fidelity } : session, ...(run ? { run: usage?.runView?.(id, run) ?? null } : {}), budget: sessionBudgetView(id) });
874
968
  }
875
- res.json({ ok: true, usage: runtime.usageView?.(id, task) ?? usage?.taskView(id, task) ?? null });
969
+ // The session budget rides along (agent_usage: the mode and each provider's share / spent / left).
970
+ res.json({ ok: true, usage: runtime.usageView?.(id, task) ?? usage?.taskView(id, task) ?? null, budget: sessionBudgetView(id) });
876
971
  } catch (error) { sendError(res, error); }
877
972
  });
878
973
  router.get('/sessions/:id', (req, res) => {
@@ -940,25 +1035,51 @@ export function createAssistantApi({
940
1035
  return (accountId) => {
941
1036
  if (dispatcher.accountInUse(provider, accountId)) return `Stop the dispatched ${provider} runs using account ${accountId} first`;
942
1037
  if (runtime?.accountInUse?.(provider, accountId)) return `Close the assistant sessions using account ${accountId} first`;
1038
+ if (provider === 'claude-code' && typeof claudeAccountInUse === 'function') return claudeAccountInUse(accountId) || null;
1039
+ if (provider === 'codex' && typeof codexAccountInUse === 'function') return codexAccountInUse(accountId) || null;
943
1040
  return null;
944
1041
  };
945
1042
  }
946
- router.get('/claude/accounts', (req, res) => {
947
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
948
- res.json({ ok: true, accounts: claudeAccounts.listForClient() });
1043
+ // The Claude account a Claude Code worker runs under when its dispatch names none (null = Default).
1044
+ function claudeWorkerAccountId() {
1045
+ try { return configStore?.workers?.().claudeAccount || null; } catch { return null; }
1046
+ }
1047
+ // The same for Codex workers.
1048
+ function codexWorkerAccountId() {
1049
+ try { return configStore?.workers?.().codexAccount || null; } catch { return null; }
1050
+ }
1051
+ const claudeUnavailable = (res) => res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1052
+ // Every Claude account as every surface shows it (lib/claude-accounts.js describe()): mode, signed in,
1053
+ // identity (an email or a masked key, never a key). `?refresh=1` asks Claude Code about each first.
1054
+ router.get('/claude/accounts', async (req, res) => {
1055
+ if (!claudeAccounts) return claudeUnavailable(res);
1056
+ try {
1057
+ if (req.query.refresh === '1' || req.query.refresh === 'true') await claudeAccounts.refreshStatus?.(null, { force: true });
1058
+ else Promise.resolve(claudeAccounts.refreshStatus?.()).catch(() => {});
1059
+ await accountUsage?.refresh('claude-code');
1060
+ res.json({ ok: true, accounts: accountUsage?.list()?.['claude-code'] || claudeAccounts.listForClient(), workerAccountId: claudeWorkerAccountId() });
1061
+ } catch (error) { sendError(res, error); }
949
1062
  });
1063
+ // Create an account. `login: true` also opens its Claude sign-in in a SynaBun terminal; otherwise the
1064
+ // caller offers the two ways to sign it in (Claude, or an API key pasted into it).
950
1065
  router.post('/claude/accounts', async (req, res) => {
951
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1066
+ if (!claudeAccounts) return claudeUnavailable(res);
952
1067
  try {
953
1068
  const account = claudeAccounts.create({ label: req.body?.label });
954
1069
  let login = null;
955
- if (req.body?.login !== false && typeof createLoginTerminal === 'function') {
956
- login = await startClaudeLogin(account.id);
957
- }
1070
+ if (req.body?.login === true) login = await startClaudeLogin(account.id);
1071
+ catalogCache = null;
958
1072
  res.status(201).json({ ok: true, account: claudeAccounts.listForClient().find((row) => row.id === account.id) || account, login });
959
1073
  } catch (error) { sendError(res, error); }
960
1074
  });
961
1075
  async function startClaudeLogin(accountId) {
1076
+ // Claude Code's own `auth login` under the account's config directory, in a SynaBun terminal (the
1077
+ // panel's sign-in). The account list says when it is signed in (claude:accounts-changed).
1078
+ if (typeof claudeSignIn === 'function') {
1079
+ const terminal = await claudeSignIn(accountId);
1080
+ if (terminal?.status === 'error') throw Object.assign(new Error(terminal.error || 'Could not open the sign-in terminal.'), { status: 500, code: 'LOGIN_FAILED' });
1081
+ return { mode: 'terminal', terminalSessionId: terminal?.sessionId || null, status: terminal?.status || 'pending', profile: 'shell' };
1082
+ }
962
1083
  const command = claudeAccounts.loginCommand(accountId);
963
1084
  const terminal = await createLoginTerminal({ env: command.env, cwd: command.cwd, profile: 'claude-code', accountId, command: command.command });
964
1085
  claudeAccounts.watchLogin(accountId, {
@@ -970,24 +1091,34 @@ export function createAssistantApi({
970
1091
  return { mode: 'terminal', terminalSessionId: terminal?.terminalSessionId || terminal?.id || null, profile: terminal?.profile || 'claude-code' };
971
1092
  }
972
1093
  router.post('/claude/accounts/:id/login', async (req, res) => {
973
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
974
- if (typeof createLoginTerminal !== 'function') return res.status(501).json({ ok: false, code: 'LOGIN_UNAVAILABLE', error: 'Login terminals are not wired' });
1094
+ if (!claudeAccounts) return claudeUnavailable(res);
1095
+ if (typeof claudeSignIn !== 'function' && typeof createLoginTerminal !== 'function') return res.status(501).json({ ok: false, code: 'LOGIN_UNAVAILABLE', error: 'Login terminals are not wired' });
975
1096
  try {
976
- if (!claudeAccounts.get(req.params.id)) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
1097
+ const row = claudeAccounts.get(req.params.id);
1098
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
977
1099
  res.json({ ok: true, login: await startClaudeLogin(req.params.id) });
978
1100
  } catch (error) { sendError(res, error); }
979
1101
  });
980
- router.patch('/claude/accounts/:id', (req, res) => {
981
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1102
+ // Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' | 'apiKey').
1103
+ router.patch('/claude/accounts/:id', async (req, res) => {
1104
+ if (!claudeAccounts) return claudeUnavailable(res);
982
1105
  try {
983
- const account = claudeAccounts.rename(req.params.id, req.body?.label);
1106
+ const row = claudeAccounts.get(req.params.id);
1107
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
1108
+ const { label, mode } = req.body || {};
1109
+ if (label === undefined && mode === undefined) return res.status(400).json({ ok: false, code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
1110
+ if (label !== undefined) claudeAccounts.rename(req.params.id, label);
1111
+ if (mode !== undefined) {
1112
+ if (typeof claudeSetMode === 'function') await claudeSetMode(req.params.id, mode);
1113
+ else claudeAccounts.setMode(req.params.id, mode);
1114
+ }
984
1115
  catalogCache = null;
985
1116
  broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: req.params.id });
986
- res.json({ ok: true, account });
1117
+ res.json({ ok: true, account: claudeAccounts.describeForClient ? claudeAccounts.describeForClient(req.params.id) : claudeAccounts.get(req.params.id) });
987
1118
  } catch (error) { sendError(res, error); }
988
1119
  });
989
1120
  router.delete('/claude/accounts/:id', (req, res) => {
990
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1121
+ if (!claudeAccounts) return claudeUnavailable(res);
991
1122
  try {
992
1123
  const result = claudeAccounts.remove(req.params.id, { inUse: accountInUse('claude-code') });
993
1124
  catalogCache = null;
@@ -995,30 +1126,89 @@ export function createAssistantApi({
995
1126
  res.json({ ok: true, ...result });
996
1127
  } catch (error) { sendError(res, error); }
997
1128
  });
998
- router.get('/codex/accounts', (req, res) => {
999
- if (!codexAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1000
- res.json({ ok: true, accounts: codexAccounts.listForClient() });
1129
+ // The account a Claude Code worker uses when its dispatch names none. Signed-out accounts are refused.
1130
+ router.put('/claude/worker-account', (req, res) => {
1131
+ if (!claudeAccounts || !configStore?.patchWorkers) return claudeUnavailable(res);
1132
+ try {
1133
+ const wanted = req.body?.accountId === null || req.body?.accountId === undefined || req.body?.accountId === '' ? 'default' : String(req.body.accountId);
1134
+ const row = claudeAccounts.get(wanted);
1135
+ if (!row || row.id !== wanted) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${wanted}` });
1136
+ const unusable = claudeAccounts.unusableReason?.(wanted) || '';
1137
+ if (unusable) return res.status(409).json({ ok: false, code: 'ACCOUNT_SIGNED_OUT', error: unusable });
1138
+ const workers = configStore.patchWorkers({ claudeAccount: wanted });
1139
+ catalogCache = null;
1140
+ broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: wanted, worker: true });
1141
+ res.json({ ok: true, workerAccountId: workers.claudeAccount });
1142
+ } catch (error) { sendError(res, error); }
1143
+ });
1144
+ const codexUnavailable = (res) => res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1145
+ // Every Codex account as every surface shows it (lib/codex-accounts.js describe()): mode, signed in,
1146
+ // identity (an email or a masked key, never a key). `?refresh=1` asks Codex about each own store first.
1147
+ router.get('/codex/accounts', async (req, res) => {
1148
+ if (!codexAccounts) return codexUnavailable(res);
1149
+ try {
1150
+ if (req.query.refresh === '1' || req.query.refresh === 'true') await codexAccounts.refreshStatus?.(null, { force: true });
1151
+ else Promise.resolve(codexAccounts.refreshStatus?.()).catch(() => {});
1152
+ await accountUsage?.refresh('codex');
1153
+ res.json({ ok: true, accounts: accountUsage?.list()?.codex || codexAccounts.listForClient(), workerAccountId: codexWorkerAccountId() });
1154
+ } catch (error) { sendError(res, error); }
1001
1155
  });
1156
+ // `codex login` under the account's CODEX_HOME, in a SynaBun terminal (the panel's sign-in). The account
1157
+ // list says when it is signed in (codex:accounts-changed).
1158
+ async function startCodexLogin(accountId) {
1159
+ if (typeof codexSignIn !== 'function') throw Object.assign(new Error('Login terminals are not wired'), { status: 501, code: 'LOGIN_UNAVAILABLE' });
1160
+ const terminal = await codexSignIn(accountId);
1161
+ if (terminal?.status === 'error') throw Object.assign(new Error(terminal.error || 'Could not open the sign-in terminal.'), { status: 500, code: 'LOGIN_FAILED' });
1162
+ return { mode: 'terminal', terminalSessionId: terminal?.sessionId || null, status: terminal?.status || 'pending', profile: 'shell' };
1163
+ }
1164
+ // Create an account. `login: true` also opens its ChatGPT sign-in in a SynaBun terminal; otherwise the
1165
+ // caller offers the two ways to sign it in (ChatGPT, or an API key pasted into it).
1166
+ router.post('/codex/accounts', async (req, res) => {
1167
+ if (!codexAccounts?.create) return codexUnavailable(res);
1168
+ try {
1169
+ const account = codexAccounts.create({ label: req.body?.label });
1170
+ const login = req.body?.login === true ? await startCodexLogin(account.id) : null;
1171
+ catalogCache = null;
1172
+ res.status(201).json({ ok: true, account: codexAccounts.describeForClient?.(account.id) || account, login });
1173
+ } catch (error) { sendError(res, error); }
1174
+ });
1175
+ // Older pages: create the account and open its ChatGPT sign-in at once.
1002
1176
  router.post('/codex/accounts/add-start', async (req, res) => {
1003
- if (!codexAccounts?.addStart) return res.status(501).json({ ok: false, code: 'NOT_IMPLEMENTED', error: 'Add a Codex account from the Codex sidepanel account menu' });
1177
+ if (!codexAccounts?.create) return codexUnavailable(res);
1004
1178
  try {
1005
- const result = await codexAccounts.addStart(req.body || {});
1179
+ const account = codexAccounts.create({ label: req.body?.label });
1180
+ const login = await startCodexLogin(account.id).catch((error) => ({ error: error.message }));
1006
1181
  catalogCache = null;
1007
- broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: result?.accountId || null });
1008
- res.status(201).json({ ok: true, ...result });
1182
+ res.status(201).json({ ok: true, accountId: account.id, account: codexAccounts.describeForClient?.(account.id) || account, login });
1183
+ } catch (error) { sendError(res, error); }
1184
+ });
1185
+ router.post('/codex/accounts/:id/login', async (req, res) => {
1186
+ if (!codexAccounts) return codexUnavailable(res);
1187
+ try {
1188
+ const row = codexAccounts.describe?.(req.params.id);
1189
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${req.params.id}` });
1190
+ res.json({ ok: true, login: await startCodexLogin(req.params.id) });
1009
1191
  } catch (error) { sendError(res, error); }
1010
1192
  });
1011
- router.patch('/codex/accounts/:id', (req, res) => {
1012
- if (!codexAccounts?.rename) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1193
+ // Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' (ChatGPT) | 'apiKey').
1194
+ router.patch('/codex/accounts/:id', async (req, res) => {
1195
+ if (!codexAccounts) return codexUnavailable(res);
1013
1196
  try {
1014
- const account = codexAccounts.rename(req.params.id, req.body?.label);
1197
+ const row = codexAccounts.describe?.(req.params.id);
1198
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${req.params.id}` });
1199
+ const { label, mode } = req.body || {};
1200
+ if (label !== undefined) codexAccounts.rename(req.params.id, label);
1201
+ if (mode !== undefined) {
1202
+ if (typeof codexSetMode === 'function') await codexSetMode(req.params.id, mode);
1203
+ else codexAccounts.setMode(req.params.id, mode);
1204
+ }
1015
1205
  catalogCache = null;
1016
1206
  broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: req.params.id });
1017
- res.json({ ok: true, account });
1207
+ res.json({ ok: true, account: codexAccounts.describeForClient(req.params.id) });
1018
1208
  } catch (error) { sendError(res, error); }
1019
1209
  });
1020
1210
  router.delete('/codex/accounts/:id', (req, res) => {
1021
- if (!codexAccounts?.remove) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1211
+ if (!codexAccounts?.remove) return codexUnavailable(res);
1022
1212
  try {
1023
1213
  const result = codexAccounts.remove(req.params.id, { inUse: accountInUse('codex') });
1024
1214
  catalogCache = null;
@@ -1026,6 +1216,38 @@ export function createAssistantApi({
1026
1216
  res.json({ ok: true, ...result });
1027
1217
  } catch (error) { sendError(res, error); }
1028
1218
  });
1219
+ // The account a Codex worker uses when its dispatch names none. Signed-out accounts are refused.
1220
+ router.put('/codex/worker-account', (req, res) => {
1221
+ if (!codexAccounts || !configStore?.patchWorkers) return codexUnavailable(res);
1222
+ try {
1223
+ const wanted = req.body?.accountId === null || req.body?.accountId === undefined || req.body?.accountId === '' ? 'default' : String(req.body.accountId);
1224
+ const row = codexAccounts.describe?.(wanted);
1225
+ if (!row || row.id !== wanted) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${wanted}` });
1226
+ const unusable = codexAccounts.unusableReason?.(wanted) || '';
1227
+ if (unusable) return res.status(409).json({ ok: false, code: 'ACCOUNT_SIGNED_OUT', error: unusable });
1228
+ const workers = configStore.patchWorkers({ codexAccount: wanted });
1229
+ catalogCache = null;
1230
+ broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: wanted, worker: true });
1231
+ res.json({ ok: true, workerAccountId: workers.codexAccount });
1232
+ } catch (error) { sendError(res, error); }
1233
+ });
1234
+
1235
+ // ── Admin sandbox (lib/agent-sandbox-setting.js) ───────────────────────────
1236
+ // Whether an agent SynaBun starts may use a sandbox whose setup needs Windows administrator
1237
+ // approval. Global, not per session. The user's setting: an agent cannot change it.
1238
+ router.get('/admin-sandbox', (req, res) => {
1239
+ if (!adminSandbox) return res.status(503).json({ ok: false, code: 'ADMIN_SANDBOX_UNAVAILABLE', error: 'The Admin sandbox setting is not available on this server' });
1240
+ res.json({ ok: true, ...adminSandbox.view() });
1241
+ });
1242
+ router.put('/admin-sandbox', requireJson, (req, res) => {
1243
+ if (!adminSandbox) return res.status(503).json({ ok: false, code: 'ADMIN_SANDBOX_UNAVAILABLE', error: 'The Admin sandbox setting is not available on this server' });
1244
+ if (callerContext(req).kind !== 'ui' || req.get('x-synabun-desktop-grant')) {
1245
+ return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the user can change the Admin sandbox.' });
1246
+ }
1247
+ const value = req.body?.adminSandbox;
1248
+ if (typeof value !== 'boolean') return res.status(400).json({ ok: false, code: 'INVALID_VALUE', error: 'adminSandbox must be true or false.' });
1249
+ try { res.json({ ok: true, ...adminSandbox.set(value) }); } catch (error) { sendError(res, error); }
1250
+ });
1029
1251
 
1030
1252
  // ── sidepanel tab registry (UI heartbeat) ──────────────────────────────────
1031
1253
  function listTabs() {
@@ -27,12 +27,14 @@ export function claudeSdkMode(approvalMode, planMode) {
27
27
  }
28
28
 
29
29
  // Never gate these behind a permission card: routing must not wait on an
30
- // approval prompt, and computer use is fully autonomous (its guards live in
31
- // the desktop service, not in the host's permission layer).
30
+ // approval prompt, computer use is fully autonomous (its guards live in
31
+ // the desktop service, not in the host's permission layer), and a file the
32
+ // user asked for on WhatsApp is the turn's reply (the WhatsApp host checks it).
32
33
  export const CLAUDE_BRAIN_ALLOWED_TOOLS = Object.freeze([
33
34
  'mcp__SynaBun__agent_route', 'mcp__SynaBun__agent_catalog', 'mcp__SynaBun__agent_status', 'mcp__SynaBun__agent_read',
34
35
  'mcp__SynaBun__agent_list', 'mcp__SynaBun__agent_wait', 'mcp__SynaBun__agent_usage',
35
36
  'mcp__SynaBun__computer', 'mcp__SynaBun__computer_apps', 'mcp__SynaBun__computer_ax', 'mcp__SynaBun__computer_status',
37
+ 'mcp__SynaBun__whatsapp_send_file',
36
38
  ]);
37
39
 
38
40
  /**
@@ -85,6 +87,10 @@ export function createClaudeBrain({ session, sink, deps = {}, persona = '', hook
85
87
  const inner = new deps.ClaudeSession(socket, {
86
88
  ownerKey: session.id,
87
89
  windowId,
90
+ // The Claude account the brain runs under: the bridge gives its process that
91
+ // account's credential (lib/claude-accounts.js launchFor(): its subscription
92
+ // login or its API key, nothing inherited), and refuses an account that is gone.
93
+ claudeAccountId: brain.accountId || 'default',
88
94
  sessionId: providerSessionId,
89
95
  model: brain.model || null,
90
96
  effort: brain.effort || null,
@@ -117,6 +123,11 @@ export function createClaudeBrain({ session, sink, deps = {}, persona = '', hook
117
123
  'X-Synabun-Role': 'assistant', 'X-Synabun-Terminal': session.id, 'X-Synabun-Memory-Session': session.id,
118
124
  ...(deps.desktopGrant ? { 'X-Synabun-Desktop-Grant': deps.desktopGrant } : {}),
119
125
  ...mcpHeaders,
126
+ // A WhatsApp conversation's brain credential (whatsapp_send_file). The header names the variable and
127
+ // the CLI expands it from its own environment (below): the MCP config is on its command line, which every
128
+ // account of the computer can read, an environment only this user's processes. The in-process MCP server
129
+ // keeps the value for the request it arrived with, and sends it on the file route alone. Last, so nothing replaces it.
130
+ ...(deps.brainCredential ? { 'X-Synabun-Brain-Token': '${SYNABUN_BRAIN_TOKEN}' } : {}),
120
131
  },
121
132
  // A remote (WhatsApp) session never pre-approves computer use: its PreToolUse hook refuses, asks or allows each call.
122
133
  allowedTools,
@@ -132,6 +143,8 @@ export function createClaudeBrain({ session, sink, deps = {}, persona = '', hook
132
143
  ...(typeof deps.claudeAccountEnv === 'function' ? deps.claudeAccountEnv(brain.accountId) : {}),
133
144
  SYNABUN_ASSISTANT_SESSION: String(session.id),
134
145
  ...(deps.userContextThreads === true ? { SYNABUN_ASSISTANT_THREADS: '1' } : {}),
146
+ // The value of the X-Synabun-Brain-Token header above (a WhatsApp conversation's brain only).
147
+ ...(deps.brainCredential ? { SYNABUN_BRAIN_TOKEN: deps.brainCredential } : {}),
135
148
  },
136
149
  maxBudgetUsd: Number(deps.maxBudgetUsd) > 0 ? Number(deps.maxBudgetUsd) : undefined,
137
150
  });
@@ -119,6 +119,9 @@ export function createCodexBrain({ session, sink, deps = {}, persona = '' } = {}
119
119
  });
120
120
  const socket = createVirtualSocket({
121
121
  name: `codex-brain-${session.id}`,
122
+ // A WhatsApp conversation's brain credential rides on the socket object, never in a message: the skin
123
+ // handler gives it to this app-server's SynaBun MCP child through its environment (whatsapp_send_file).
124
+ credential: deps.brainCredential || null,
122
125
  onSend: (packet) => {
123
126
  if (disposed || !packet || typeof packet !== 'object') return;
124
127
  if (packet.type === 'ready') readyResolve?.();