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
@@ -0,0 +1,524 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — WhatsApp Link: a local file the Assistant sends to the owner
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // whatsapp_send_file (docs/whatsapp.md → Sending files). This runs in the
6
+ // WhatsApp host, inside the send queue (host-core.js sendToOwner), right
7
+ // before the file goes out: the main process never opens it. A refusal at
8
+ // any step sends nothing.
9
+ //
10
+ // 1. The request (checkRequestPath, planFileSend): an absolute path of at
11
+ // most 2048 characters without control characters; never a URL, a
12
+ // network share or device path, a system path (/dev, /proc, /sys and the
13
+ // macOS /.vol, /.nofollow, /.resolve), or (Windows) an alternate data
14
+ // stream. `as` auto picks the form by extension (FILE_TYPES); a native
15
+ // form that does not match the extension is refused ("use auto or
16
+ // document"); a photo, video or audio file over 16 MiB goes as a
17
+ // document; native audio has no caption.
18
+ // 2. The denylist (fileDenial), on the path as written AND on the real file
19
+ // it leads to (symlinks resolved; a share or a system path there is
20
+ // refused too): the protected roots (the WhatsApp session, SynaBun's data
21
+ // home except its generated media in data/media) and explicit families of
22
+ // secrets, by place and by name, for macOS, Linux and Windows (FAMILIES).
23
+ // Matching ignores letter case. The roots and the names right under the
24
+ // home are decided again by device and inode (placeDenial), because the
25
+ // disk takes other spellings of a folder that realpath keeps (macOS:
26
+ // /System/Volumes/Data/Users/… is /Users/…).
27
+ // 3. The read (prepareLocalFile): a regular, non-empty file with one hard
28
+ // link, at most 100 MiB (refused before anything is read), opened
29
+ // without following a link (O_NOFOLLOW where the platform has it, and
30
+ // O_NONBLOCK so a FIFO swapped in cannot hang the open), still the file
31
+ // the checks saw (device, inode, size), its real path unchanged and
32
+ // still naming the opened file (a parent folder swapped for a link and
33
+ // back), on Linux the open file's own name (/proc/self/fd) checked too,
34
+ // read to its end and not one byte further, and unchanged when the read
35
+ // ends. A process of the same user that wins a run of precisely timed
36
+ // swaps between these checks is the limit of what a path check can do.
37
+ //
38
+ // The content is never inspected: a secret inside an ordinary file under an
39
+ // ordinary name is sent like any other file. The lists are explicit, a
40
+ // speed bump with a long list of known places, not a sandbox (SECURITY.md).
41
+
42
+ import { constants as fsConstants, promises as fsp, realpathSync } from 'node:fs';
43
+ import { homedir } from 'node:os';
44
+ import path from 'node:path';
45
+ import { ERR, FILE_SEND_AS, LIMITS } from './protocol.js';
46
+
47
+ /** A refusal: `code` is a protocol.js ERR code (BAD_REQUEST, FILE_FORBIDDEN, FILE_TOO_LARGE, FILE_INVALID). */
48
+ export class LocalFileError extends Error {
49
+ constructor(code, message) {
50
+ super(message);
51
+ this.name = 'LocalFileError';
52
+ this.code = code;
53
+ }
54
+ }
55
+ const refuse = (code, message) => { throw new LocalFileError(code, message); };
56
+
57
+ // ── types ────────────────────────────────────────────────────────────────────
58
+
59
+ const TYPE_ROWS = [
60
+ ['jpg', 'image', 'image/jpeg'], ['jpeg', 'image', 'image/jpeg'], ['png', 'image', 'image/png'], ['webp', 'image', 'image/webp'],
61
+ ['mp4', 'video', 'video/mp4'],
62
+ ['ogg', 'audio', 'audio/ogg'], ['opus', 'audio', 'audio/ogg'], ['mp3', 'audio', 'audio/mpeg'], ['m4a', 'audio', 'audio/mp4'],
63
+ ['pdf', 'document', 'application/pdf'], ['md', 'document', 'text/markdown'], ['patch', 'document', 'text/x-diff'], ['diff', 'document', 'text/x-diff'],
64
+ ['txt', 'document', 'text/plain'], ['csv', 'document', 'text/csv'], ['json', 'document', 'application/json'], ['zip', 'document', 'application/zip'],
65
+ ['doc', 'document', 'application/msword'], ['xls', 'document', 'application/vnd.ms-excel'],
66
+ ['docx', 'document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
67
+ ['xlsx', 'document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
68
+ ['pptx', 'document', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
69
+ ['gif', 'document', 'image/gif'], ['svg', 'document', 'image/svg+xml'], ['wav', 'document', 'audio/wav'], ['webm', 'document', 'video/webm'],
70
+ ['7z', 'document', 'application/x-7z-compressed'], ['gz', 'document', 'application/gzip'],
71
+ ];
72
+ /** Extension → { type, mimetype }: image, video and audio go in their native form under `as: 'auto'`, the rest as documents. */
73
+ export const FILE_TYPES = Object.freeze(Object.fromEntries(TYPE_ROWS.map(([ext, type, mimetype]) => [ext, Object.freeze({ type, mimetype })])));
74
+ export const UNKNOWN_MIMETYPE = 'application/octet-stream';
75
+
76
+ /** The part after the last dot, lower case ('' without one). */
77
+ function extOf(name) {
78
+ const text = String(name || '').toLowerCase();
79
+ const dot = text.lastIndexOf('.');
80
+ return dot >= 0 ? text.slice(dot + 1) : '';
81
+ }
82
+
83
+ /** A file name → { ext, detected: image|video|audio|document, mimetype }. */
84
+ export function fileTypeOf(name) {
85
+ const ext = extOf(name);
86
+ const known = Object.hasOwn(FILE_TYPES, ext) ? FILE_TYPES[ext] : null;
87
+ return { ext, detected: known ? known.type : 'document', mimetype: known ? known.mimetype : UNKNOWN_MIMETYPE };
88
+ }
89
+
90
+ const A = Object.freeze({ image: 'an image', video: 'a video', audio: 'an audio', document: 'a document' });
91
+
92
+ /**
93
+ * How a file goes, from its name and size → { type, detected, mimetype, fallback, fileName, caption }.
94
+ * type `as: 'document'`, or more than 16 MiB → document; else what the extension says
95
+ * fallback a photo, video or audio file that goes as a document only because of its size
96
+ * caption null for native audio (refused when one was given) and for a blank one
97
+ * Throws BAD_REQUEST: an unknown `as`, a native form that does not match the extension, a caption on
98
+ * native audio, a caption over 1024 characters.
99
+ */
100
+ export function planFileSend({ name, size = 0, as = 'auto', caption = null } = {}) {
101
+ const mode = as === undefined || as === null ? 'auto' : as;
102
+ if (!FILE_SEND_AS.includes(mode)) refuse(ERR.BAD_REQUEST, `as must be one of ${FILE_SEND_AS.join(', ')}`);
103
+ if (caption !== null && caption !== undefined && typeof caption !== 'string') refuse(ERR.BAD_REQUEST, 'caption must be text');
104
+ const text = typeof caption === 'string' ? caption : '';
105
+ if (text.length > LIMITS.MAX_FILE_CAPTION) refuse(ERR.BAD_REQUEST, `caption is longer than ${LIMITS.MAX_FILE_CAPTION} characters`);
106
+ const fileName = String(name || 'file');
107
+ const { detected, mimetype } = fileTypeOf(fileName);
108
+ if (mode !== 'auto' && mode !== 'document' && mode !== detected) {
109
+ refuse(ERR.BAD_REQUEST, `${fileName} is not ${A[mode]} file by its extension (it would go as ${A[detected]}): use as "auto" or "document"`);
110
+ }
111
+ const type = mode === 'document' || Number(size) > LIMITS.MAX_FILE_MEDIA_BYTES ? 'document' : detected;
112
+ const fallback = type === 'document' && detected !== 'document' && mode !== 'document';
113
+ if (type === 'audio' && text.trim()) refuse(ERR.BAD_REQUEST, 'audio goes without a caption: leave the caption out, or send it as "document"');
114
+ return { type, detected, mimetype, fallback, fileName, caption: type !== 'audio' && text.trim() ? text : null };
115
+ }
116
+
117
+ // ── the request ──────────────────────────────────────────────────────────────
118
+
119
+ const CONTROL = /[\u0000-\u001f\u007f]/;
120
+ const URL_LIKE = /^[a-z][a-z0-9+.-]+:/i; // http:, file:, smb:, data: … (a drive letter is one letter)
121
+ const SHARE_OR_DEVICE = /^(?:[\\/]{2}|\\\?\?\\)/; // \\server\share, //server, \\?\, \\.\, \??\
122
+ // The system's own namespaces (macOS and Linux), never a file of the user's: devices and the open files of
123
+ // a process (/dev/fd/3 is the host's own descriptor 3), process and kernel files, and the macOS kernel
124
+ // prefixes that open any file under another spelling (/.vol/<device>/<inode>, /.nofollow/…, /.resolve/…).
125
+ const SYSTEM_TOPS = new Set(['dev', 'proc', 'sys', '.vol', '.nofollow', '.resolve']);
126
+ const systemPath = (abs) => SYSTEM_TOPS.has(String(abs).split('/')[1]?.toLowerCase());
127
+
128
+ /**
129
+ * The path a request names, checked for its shape alone (no filesystem access) → the absolute path,
130
+ * normalized (`.` and `..` resolved lexically). BAD_REQUEST: not text, empty, too long, a control
131
+ * character, not absolute (on Windows: no drive letter). FILE_FORBIDDEN: a URL, a network share or a
132
+ * device path, an alternate data stream (Windows), a system path (/dev, /proc, /sys, /.vol, …).
133
+ */
134
+ export function checkRequestPath(value, { platform = process.platform } = {}) {
135
+ if (typeof value !== 'string' || !value.trim()) refuse(ERR.BAD_REQUEST, 'path must be the absolute path of a file on this computer');
136
+ if (value.length > LIMITS.MAX_FILE_PATH) refuse(ERR.BAD_REQUEST, `path is longer than ${LIMITS.MAX_FILE_PATH} characters`);
137
+ if (CONTROL.test(value)) refuse(ERR.BAD_REQUEST, 'path holds a control character');
138
+ const win = platform === 'win32';
139
+ if (SHARE_OR_DEVICE.test(value)) refuse(ERR.FILE_FORBIDDEN, `${value} is a network share or a device path: only a file on this computer's own disk is sent`);
140
+ if (URL_LIKE.test(value)) refuse(ERR.FILE_FORBIDDEN, `${value} is a URL: only a file on this computer's own disk is sent`);
141
+ const p = win ? path.win32 : path.posix;
142
+ if (win) {
143
+ if (!/^[a-z]:[\\/]/i.test(value)) refuse(ERR.BAD_REQUEST, 'path must be absolute, starting with a drive letter (C:\\…)');
144
+ // C:\file.txt:stream — any ':' after the drive's own names an alternate data stream.
145
+ if (value.indexOf(':', 2) !== -1) refuse(ERR.FILE_FORBIDDEN, `${value} names an alternate data stream: only a whole file is sent`);
146
+ } else if (!p.isAbsolute(value)) {
147
+ refuse(ERR.BAD_REQUEST, 'path must be absolute (start with /)');
148
+ }
149
+ const resolved = p.resolve(value);
150
+ if (!win && systemPath(resolved)) refuse(ERR.FILE_FORBIDDEN, `${value} is a system path (a device, a process's files): only a file on this computer's own disk is sent`);
151
+ return resolved;
152
+ }
153
+
154
+ // ── the denylist ─────────────────────────────────────────────────────────────
155
+
156
+ const escapeRe = (text) => text.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
157
+ /** "library/application support/google/chrome*" → one RegExp per path segment (`*`: any run inside a segment). */
158
+ const run = (spec) => spec.split('/').map((segment) => new RegExp(`^${segment.split('*').map(escapeRe).join('[^/]*')}$`));
159
+
160
+ /**
161
+ * The families of secrets, by place (a run of path segments anywhere in the path), by file name, by
162
+ * name pattern and by extension. Every value is lower case; matching lower-cases the path.
163
+ */
164
+ const FAMILY_ROWS = [
165
+ {
166
+ why: 'a browser profile (saved passwords, cookies and sessions)',
167
+ places: [
168
+ // macOS
169
+ 'library/application support/google/chrome*', 'library/application support/chromium', 'library/application support/bravesoftware',
170
+ 'library/application support/microsoft edge*', 'library/application support/firefox', 'library/application support/arc',
171
+ 'library/application support/com.operasoftware.opera*', 'library/application support/vivaldi', 'library/safari',
172
+ 'library/containers/com.apple.safari', 'library/cookies', 'library/httpstorages',
173
+ // Linux (native, snap and flatpak)
174
+ '.config/google-chrome*', '.config/chromium', '.config/bravesoftware', '.config/microsoft-edge*', '.mozilla',
175
+ '.config/opera*', '.config/vivaldi*', 'snap/firefox', 'snap/chromium', '.var/app/org.mozilla.firefox', '.var/app/com.google.chrome',
176
+ '.var/app/org.chromium.chromium', '.var/app/com.brave.browser', '.var/app/com.microsoft.edge', '.var/app/com.opera.opera', '.var/app/com.vivaldi.vivaldi',
177
+ // Windows
178
+ 'appdata/local/google/chrome*', 'appdata/local/chromium', 'appdata/local/bravesoftware', 'appdata/local/microsoft/edge*',
179
+ 'appdata/roaming/mozilla', 'appdata/local/mozilla', 'appdata/roaming/opera software', 'appdata/local/vivaldi', 'appdata/local/packages/thebrowsercompany.arc*',
180
+ // Anti-detect browsers (MoreLogin) and SynaBun's own browser profiles, wherever they are
181
+ '*morelogin*', 'browser-profiles',
182
+ ],
183
+ names: ['logins.json', 'key3.db', 'key4.db', 'cert8.db', 'cert9.db', 'signons.sqlite', 'cookies.sqlite', 'login data', 'login data for account', 'web data', 'cookies', 'cookies.binarycookies', 'local state'],
184
+ },
185
+ {
186
+ why: 'an app\'s sign-in store (Discord, Slack, Telegram, Signal, WhatsApp Desktop)',
187
+ places: [
188
+ 'library/application support/discord*', '.config/discord*', 'appdata/roaming/discord*', '.var/app/com.discordapp.discord',
189
+ 'library/application support/slack', 'library/containers/com.tinyspeck.slackmacgap', '.config/slack', 'appdata/roaming/slack', 'appdata/local/packages/91750d7e.slack*', '.var/app/com.slack.slack',
190
+ 'tdata', 'library/application support/telegram desktop', 'library/group containers/*.ru.keepcoder.telegram', '.local/share/telegramdesktop', 'appdata/roaming/telegram desktop', '.var/app/org.telegram.desktop',
191
+ 'library/application support/signal', '.config/signal', 'appdata/roaming/signal', '.var/app/org.signal.signal',
192
+ 'library/group containers/group.net.whatsapp*', 'library/containers/net.whatsapp.whatsapp', 'library/containers/desktop.whatsapp', 'library/application support/whatsapp',
193
+ 'appdata/local/packages/5319275a.whatsappdesktop*', 'appdata/roaming/whatsapp', '.config/whatsapp*',
194
+ ],
195
+ },
196
+ {
197
+ why: 'a password manager or a keychain',
198
+ places: ['*1password*', '*agilebits*', '*bitwarden*', 'library/keychains', '.password-store', '.config/op', 'keepassxc', '.keepassxc',
199
+ // GNOME Keyring, KWallet; Windows Credential Manager, its DPAPI master keys and the Vault
200
+ '.local/share/keyrings', '.local/share/kwalletd', '.kde/share/apps/kwallet', '.kde4/share/apps/kwallet',
201
+ 'appdata/roaming/microsoft/credentials', 'appdata/local/microsoft/credentials', 'appdata/roaming/microsoft/protect',
202
+ 'appdata/local/microsoft/vault', 'appdata/roaming/microsoft/vault'],
203
+ exts: ['kdbx', 'kdb', 'keychain', 'keychain-db', 'keyring', 'kwl'],
204
+ },
205
+ {
206
+ why: 'a mail or message store',
207
+ places: [
208
+ 'library/mail', 'library/containers/com.apple.mail', 'library/group containers/group.com.apple.mail', 'library/messages',
209
+ 'library/containers/com.apple.ichat', 'library/containers/com.apple.messages',
210
+ '.thunderbird', 'library/thunderbird', 'appdata/roaming/thunderbird', '.var/app/org.mozilla.thunderbird', 'snap/thunderbird',
211
+ ],
212
+ },
213
+ {
214
+ why: 'a shell history',
215
+ places: ['.zsh_sessions'],
216
+ names: ['.zsh_history', '.zhistory', '.bash_history', '.sh_history', '.history', 'fish_history', '.python_history', '.node_repl_history',
217
+ '.psql_history', '.mysql_history', '.sqlite_history', '.rediscli_history', '.irb_history', 'consolehost_history.txt'],
218
+ },
219
+ {
220
+ why: 'credentials (SSH, GnuPG, cloud, registry and git logins)',
221
+ places: ['.ssh', '.gnupg', '.aws', '.azure', '.docker', '.kube', '.config/gcloud', '.config/gh', 'appdata/roaming/gcloud', 'appdata/roaming/github cli', 'appdata/roaming/gnupg', '.terraform.d',
222
+ // CLI token stores: git's own (XDG), cargo, gem, Pulumi, Hugging Face, Kaggle, rclone, hub, glab, doctl, Stripe, Fly, cloudflared, OCI
223
+ '.config/git/credentials', '.cargo/credentials', '.cargo/credentials.toml', '.gem/credentials', '.pulumi/credentials.json', '.huggingface/token',
224
+ '.cache/huggingface/token', '.kaggle/kaggle.json', '.config/rclone', '.config/hub', '.config/glab-cli', '.config/doctl', '.config/stripe', '.fly', '.cloudflared', '.oci'],
225
+ names: ['.netrc', '_netrc', '.npmrc', '.pypirc', '.pgpass', 'pgpass.conf', '.git-credentials', '.my.cnf', '.boto', '.s3cfg', '.dockercfg', 'kubeconfig',
226
+ 'authorized_keys', 'authorized_keys2', '.terraformrc', 'terraform.rc', 'credentials.tfrc.json', '.vault-token', '.databrickscfg'],
227
+ patterns: [/^id_(?:rsa|dsa|ecdsa|ed25519|xmss)(?:_sk)?(?:-cert)?(?:\.pub)?$/, /\.kubeconfig$/],
228
+ },
229
+ {
230
+ why: 'a key or certificate file',
231
+ exts: ['pem', 'key', 'p12', 'pfx', 'jks', 'keystore', 'ppk', 'p8', 'pkcs12'],
232
+ },
233
+ {
234
+ why: 'an environment file (.env)',
235
+ names: ['.env', '.envrc'],
236
+ patterns: [/^\.env\./, /\.env$/],
237
+ },
238
+ {
239
+ why: 'terraform state',
240
+ places: ['terraform.tfstate.d'],
241
+ patterns: [/\.tfstate(?:\.backup)?$/],
242
+ },
243
+ {
244
+ why: 'an AI tool\'s configuration or credentials',
245
+ places: ['.cursor/mcp.json', '.config/opencode', '.local/share/opencode', '.local/state/opencode', 'library/application support/opencode', 'appdata/roaming/opencode', 'appdata/local/opencode',
246
+ 'library/application support/claude', 'appdata/roaming/claude', '.config/claude', '.claude-accounts', '.codex-accounts', '.codex*/auth.json', '.gemini',
247
+ '.config/github-copilot', 'appdata/local/github-copilot',
248
+ // ~/.claude and ~/.codex are protected whole (by identity too); their secret parts also wherever a copy of them is (a backup)
249
+ '.claude/settings*.json', '.claude/projects', '.codex*/config.toml', '.codex*/sessions', '.codex*/history.jsonl'],
250
+ names: ['.claude.json', '.credentials.json', 'claude_desktop_config.json', '.synabun-api-key'],
251
+ patterns: [/^\.claude\.json\./, /mcp-api-key/],
252
+ },
253
+ {
254
+ why: 'SynaBun\'s own data (memory database, WhatsApp session, backups)',
255
+ // The data home is protected whole (protectedPlaces); its secret parts also wherever a copy of them is, and SynaBun's
256
+ // backup archives (they hold .env and the memory database) by their name wherever the backup folder is.
257
+ places: ['.synabun/whatsapp', '.synabun/mcp-data', '.synabun/runtime', '.synabun/backups', 'sidepanel-configs'],
258
+ names: ['memory.db', 'memory.db-wal', 'memory.db-shm', 'memory.db-journal', 'state.db', 'state.db-wal', 'state.db-shm', 'state.db-journal', 'synabun-auto-backup.zip'],
259
+ patterns: [/^synabun-[a-z-]+-\d{4}-\d{2}-\d{2}t\d{2}-\d{2}-\d{2}-\d{3}z\.zip(?:\.\d+\.tmp)?$/],
260
+ },
261
+ ];
262
+ const FAMILIES = FAMILY_ROWS.map((row) => Object.freeze({
263
+ why: row.why,
264
+ places: (row.places || []).map(run),
265
+ names: new Set(row.names || []),
266
+ patterns: row.patterns || [],
267
+ exts: new Set(row.exts || []),
268
+ }));
269
+ /** What the denylist protects, family by family (docs and tests read it). */
270
+ export const DENYLIST = Object.freeze(FAMILY_ROWS.map((row) => Object.freeze({
271
+ why: row.why, places: Object.freeze([...(row.places || [])]), names: Object.freeze([...(row.names || [])]),
272
+ patterns: Object.freeze((row.patterns || []).map(String)), exts: Object.freeze([...(row.exts || [])]),
273
+ })));
274
+
275
+ // Entries right under the user's home: ~/.claude* (settings, credentials, ~/.claude.json) and ~/.codex*.
276
+ // (~/.synabun, the default data home, is a protected root of its own: protectedPlaces.)
277
+ const HOME_ENTRIES = Object.freeze([
278
+ { re: /^\.claude/, why: 'an AI tool\'s configuration or credentials (~/.claude*)' },
279
+ { re: /^\.codex/, why: 'an AI tool\'s configuration or credentials (~/.codex*)' },
280
+ ]);
281
+ // A data home that holds the user's home (or the disk's root) is protected by these folders only.
282
+ const DATA_HOME_CHILDREN = Object.freeze(['data', 'mcp-data', 'runtime', 'whatsapp', 'cache', 'backups', 'bin', '.env']);
283
+
284
+ /** A path as the denylist compares it: forward slashes, no doubled or trailing slash, lower case. */
285
+ export function denylistForm(value) {
286
+ let text = String(value || '').replace(/\\/g, '/').replace(/\/{2,}/g, '/').toLowerCase();
287
+ if (text.length > 1 && text.endsWith('/') && !/^[a-z]:\/$/.test(text)) text = text.slice(0, -1);
288
+ return text;
289
+ }
290
+ const insideForm = (child, parent) => !!parent && (child === parent || child.startsWith(parent.endsWith('/') ? parent : `${parent}/`));
291
+
292
+ function defaultRealpath(value) {
293
+ return realpathSync.native ? realpathSync.native(value) : realpathSync(value);
294
+ }
295
+
296
+ /**
297
+ * Where the denylist looks: the user's home and the protected roots, each as written and, when it
298
+ * exists, as its real path (so a link to a protected folder is caught both ways).
299
+ * roots the WhatsApp session (waHome, authDir) and SynaBun's data homes, whole: DATA_HOME and
300
+ * ~/.synabun (the default one, protected when DATA_HOME is elsewhere too)
301
+ * allowed <data home>/data/media: the images and videos SynaBun collected from worker runs and
302
+ * the saved browser screenshots, the files the Assistant is asked to send
303
+ * folders the same folders as written (home, root, allowed), for the check by device and inode that
304
+ * prepareLocalFile makes (placeDenial): a spelling the disk accepts for them is caught too
305
+ * `paths`: resolveWhatsAppPaths()-shaped ({ dataHome, waHome, authDir }); `realpath` throws or returns.
306
+ */
307
+ export function protectedPlaces({ paths = {}, home = homedir(), platform = process.platform, realpath = defaultRealpath } = {}) {
308
+ const p = platform === 'win32' ? path.win32 : path.posix;
309
+ const variants = (value) => {
310
+ if (typeof value !== 'string' || !value) return [];
311
+ const abs = p.resolve(value);
312
+ const out = [denylistForm(abs)];
313
+ try {
314
+ const real = realpath(abs);
315
+ if (typeof real === 'string' && real) out.push(denylistForm(real));
316
+ } catch { /* not there (yet): its written form is enough */ }
317
+ return [...new Set(out)];
318
+ };
319
+ const folders = [];
320
+ const folder = (dir, kind, why = null, media = false) => folders.push(Object.freeze({ path: p.resolve(dir), kind, why, media }));
321
+ const homes = variants(home);
322
+ if (typeof home === 'string' && home) folder(home, 'home');
323
+ const roots = [];
324
+ for (const dir of [paths.authDir, paths.waHome]) {
325
+ for (const form of variants(dir)) roots.push(Object.freeze({ path: form, why: 'the WhatsApp session', media: false }));
326
+ if (typeof dir === 'string' && dir) folder(dir, 'root', 'the WhatsApp session', false);
327
+ }
328
+ const allowed = [];
329
+ const dataHomes = [paths.dataHome, typeof home === 'string' && home ? p.join(p.resolve(home), '.synabun') : null].filter((dir) => typeof dir === 'string' && dir);
330
+ for (const dataHome of [...new Set(dataHomes.map((dir) => p.resolve(dir)))]) {
331
+ const holdsHome = variants(dataHome).some((form) => homes.some((h) => insideForm(h, form)));
332
+ const dirs = holdsHome ? DATA_HOME_CHILDREN.map((child) => p.join(dataHome, child)) : [dataHome];
333
+ // `media`: the data home's one open folder (allowed) is open inside this root only.
334
+ for (const dir of dirs) {
335
+ for (const form of variants(dir)) roots.push(Object.freeze({ path: form, why: 'SynaBun\'s own data folder', media: true }));
336
+ folder(dir, 'root', 'SynaBun\'s own data folder', true);
337
+ }
338
+ allowed.push(...variants(p.join(dataHome, 'data', 'media')));
339
+ folder(p.join(dataHome, 'data', 'media'), 'allowed');
340
+ }
341
+ return Object.freeze({ homes: Object.freeze(homes), roots: Object.freeze(roots), allowed: Object.freeze([...new Set(allowed)]), folders: Object.freeze(folders) });
342
+ }
343
+
344
+ /**
345
+ * Why this path is never sent, or null. Checks one path: call it on the path as written and on its
346
+ * real path. Also checks the path with each name's trailing dots and spaces dropped, the spelling
347
+ * Windows opens (`.ssh.\id_rsa` is `.ssh\id_rsa`). `places`: protectedPlaces().
348
+ */
349
+ export function fileDenial(candidate, places = { homes: [], roots: [], allowed: [] }) {
350
+ const form = denylistForm(candidate);
351
+ const trimmed = form.split('/').map((segment) => segment.replace(/[. ]+$/, '') || segment).join('/');
352
+ return denialOf(form, places) || (trimmed !== form ? denialOf(trimmed, places) : null);
353
+ }
354
+
355
+ /** Why a name right under the user's home is never sent (~/.claude*, ~/.codex*), or null. */
356
+ function homeEntryDenial(first) {
357
+ const trimmed = first.replace(/[. ]+$/, '') || first;
358
+ for (const entry of HOME_ENTRIES) if (entry.re.test(first) || entry.re.test(trimmed)) return entry.why;
359
+ return null;
360
+ }
361
+
362
+ function denialOf(form, places) {
363
+ const segments = form.split('/').filter(Boolean);
364
+ const name = segments.length ? segments[segments.length - 1] : '';
365
+ const media = (places.allowed || []).some((dir) => insideForm(form, dir));
366
+ for (const root of places.roots || []) if (insideForm(form, root.path) && !(media && root.media)) return root.why;
367
+ for (const home of places.homes || []) {
368
+ if (form === home || !insideForm(form, home)) continue;
369
+ const why = homeEntryDenial(form.slice(home.endsWith('/') ? home.length : home.length + 1).split('/')[0]);
370
+ if (why) return why;
371
+ }
372
+ const ext = extOf(name);
373
+ for (const family of FAMILIES) {
374
+ if (family.names.has(name) || family.exts.has(ext) || family.patterns.some((re) => re.test(name))) return family.why;
375
+ for (const place of family.places) {
376
+ for (let i = 0; i + place.length <= segments.length; i += 1) {
377
+ if (place.every((re, j) => re.test(segments[i + j]))) return family.why;
378
+ }
379
+ }
380
+ }
381
+ return null;
382
+ }
383
+
384
+ // ── the read ─────────────────────────────────────────────────────────────────
385
+
386
+ // O_NOFOLLOW: a link swapped in after the checks is refused at open. O_NONBLOCK: a FIFO swapped in
387
+ // cannot hang the open (the fstat that follows refuses it). 0 where the platform lacks them (Windows:
388
+ // there the device / inode comparison catches a swap).
389
+ const OPEN_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0) | (fsConstants.O_NONBLOCK || 0);
390
+
391
+ const sizeText = (bytes) => (bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MiB` : bytes >= 1024 ? `${Math.round(bytes / 1024)} KiB` : `${bytes} bytes`);
392
+ // A path in a refusal: its end when it is long, so the reason after it fits the reply (500 characters).
393
+ const shown = (value) => { const text = String(value ?? ''); return text.length > 220 ? `…${text.slice(-220)}` : text; };
394
+
395
+ /** A folder as the disk knows it, device and inode (as file-explorer-api.js entryId says them), or null. */
396
+ async function folderId(dir, fs) {
397
+ try {
398
+ const st = await fs.stat(dir, { bigint: true });
399
+ return st.isDirectory() && st.ino ? `${st.dev}:${st.ino}` : null;
400
+ } catch { return null; }
401
+ }
402
+
403
+ /**
404
+ * The rules tied to a place (the protected roots, data/media, the names right under the home) decided by
405
+ * what each folder above the file is on disk, never by how the path spells it: macOS reaches every folder
406
+ * of the data volume under a second name that realpath keeps (/System/Volumes/Data/Users/… is /Users/…,
407
+ * and so is /.nofollow/Users/…), a case-insensitive disk takes any letter case, Windows a short 8.3 name.
408
+ * `real`: the file's real path; `places`: protectedPlaces() (its `folders`). Why it is never sent, or null.
409
+ */
410
+ async function placeDenial(real, places, platform, fs) {
411
+ const p = platform === 'win32' ? path.win32 : path.posix;
412
+ const marks = new Map();
413
+ for (const dir of places.folders || []) {
414
+ const id = await folderId(dir.path, fs);
415
+ if (id) marks.set(id, [...(marks.get(id) || []), dir]);
416
+ }
417
+ if (!marks.size) return null;
418
+ // Every folder above the file, nearest first, with the name the way to the file takes below it.
419
+ const chain = [];
420
+ for (let below = real, at = p.dirname(real); ; below = at, at = p.dirname(at)) {
421
+ chain.push({ marked: marks.get(await folderId(at, fs)) || [], name: denylistForm(p.basename(below)) });
422
+ if (p.dirname(at) === at) break;
423
+ }
424
+ const media = chain.some((step) => step.marked.some((dir) => dir.kind === 'allowed'));
425
+ for (const step of chain) {
426
+ for (const dir of step.marked) {
427
+ if (dir.kind === 'root' && !(media && dir.media)) return dir.why;
428
+ if (dir.kind === 'home') { const why = homeEntryDenial(step.name); if (why) return why; }
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+
434
+ /**
435
+ * The path of an open file as the system names it, where it can (Linux: /proc/self/fd), else null:
436
+ * whatever path led to it, the file itself is checked against the list too.
437
+ */
438
+ async function openFilePath(handle, platform, fs) {
439
+ if (platform !== 'linux' || typeof handle?.fd !== 'number' || typeof fs.readlink !== 'function') return null;
440
+ try { return await fs.readlink(`/proc/self/fd/${handle.fd}`); } catch { return null; }
441
+ }
442
+
443
+ /**
444
+ * Check and read one local file for whatsapp_send_file → { path (canonical), size, fileName, type,
445
+ * mimetype, caption, fallback, data }. Throws LocalFileError: BAD_REQUEST (the request),
446
+ * FILE_FORBIDDEN (a protected place, a URL / share / stream, a file with more than one hard link),
447
+ * FILE_TOO_LARGE (over 100 MiB, before reading), FILE_INVALID (missing, unreadable, a folder or
448
+ * anything but a regular file, empty, or changed while it was read).
449
+ * @param {string} requested the path as the request named it
450
+ * @param {object} [o]
451
+ * @param {string} [o.as] auto | document | image | video | audio
452
+ * @param {string} [o.caption]
453
+ * @param {object} [o.paths] resolveWhatsAppPaths()-shaped (the protected roots)
454
+ * @param {string} [o.home]
455
+ * @param {object} [o.fs] node:fs/promises-shaped (tests)
456
+ */
457
+ export async function prepareLocalFile(requested, { as = 'auto', caption = null, paths = {}, home = homedir(), platform = process.platform, fs = fsp, realpath = defaultRealpath } = {}) {
458
+ const written = checkRequestPath(requested, { platform });
459
+ const places = protectedPlaces({ paths, home, platform, realpath });
460
+ const forbidden = (target, why) => refuse(ERR.FILE_FORBIDDEN, `${shown(target)} is protected (${why}): SynaBun never sends it from WhatsApp`);
461
+ const deniedWritten = fileDenial(written, places);
462
+ if (deniedWritten) forbidden(written, deniedWritten);
463
+ let real;
464
+ try { real = await fs.realpath(written); }
465
+ catch (error) { refuse(ERR.FILE_INVALID, error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? `${shown(written)} does not exist` : `${shown(written)} could not be read (${error?.code || 'error'})`); }
466
+ if (typeof real !== 'string' || !real || real.length > LIMITS.MAX_FILE_PATH) refuse(ERR.FILE_INVALID, `${shown(written)} leads to a path longer than ${LIMITS.MAX_FILE_PATH} characters`);
467
+ const target = real === written ? written : `${shown(written)} (a link to ${real})`;
468
+ // Where it leads, by its shape: a mapped drive or a link can lead to a share or into /dev.
469
+ if (SHARE_OR_DEVICE.test(real) || (platform !== 'win32' && systemPath(real))) {
470
+ refuse(ERR.FILE_FORBIDDEN, `${shown(target)} is on a network share or is a system path: only a file on this computer's own disk is sent`);
471
+ }
472
+ const deniedReal = fileDenial(real, places) || await placeDenial(real, places, platform, fs);
473
+ if (deniedReal) forbidden(target, deniedReal);
474
+ let seen;
475
+ try { seen = await fs.lstat(real); }
476
+ catch (error) { refuse(ERR.FILE_INVALID, `${shown(written)} could not be read (${error?.code || 'error'})`); }
477
+ if (seen.isDirectory()) refuse(ERR.FILE_INVALID, `${shown(written)} is a folder: send one file (zip the folder first)`);
478
+ if (!seen.isFile()) refuse(ERR.FILE_INVALID, `${shown(written)} is not a regular file`);
479
+ const linked = () => refuse(ERR.FILE_FORBIDDEN, `${shown(written)} has more than one hard link: its other names cannot be checked against the protected places, so it is not sent`);
480
+ if (seen.nlink > 1) linked();
481
+ if (seen.size > LIMITS.MAX_FILE_BYTES) refuse(ERR.FILE_TOO_LARGE, `${shown(written)} is ${sizeText(seen.size)}: WhatsApp files from SynaBun are limited to ${sizeText(LIMITS.MAX_FILE_BYTES)}`);
482
+ if (seen.size === 0) refuse(ERR.FILE_INVALID, `${shown(written)} is empty`);
483
+ const fileName = (platform === 'win32' ? path.win32 : path.posix).basename(real);
484
+ const plan = planFileSend({ name: fileName, size: seen.size, as, caption });
485
+
486
+ let handle;
487
+ try { handle = await fs.open(real, OPEN_FLAGS); }
488
+ catch (error) { refuse(ERR.FILE_INVALID, error?.code === 'ELOOP' ? `${shown(written)} became a link while it was checked: nothing was sent` : `${shown(written)} could not be opened (${error?.code || 'error'})`); }
489
+ const changed = () => refuse(ERR.FILE_INVALID, `${shown(written)} changed while it was read: nothing was sent`);
490
+ try {
491
+ const opened = await handle.stat();
492
+ if (!opened.isFile() || opened.dev !== seen.dev || opened.ino !== seen.ino || opened.size !== seen.size) changed();
493
+ if (opened.nlink > 1) linked();
494
+ let again = null;
495
+ try { again = await fs.realpath(written); } catch { again = null; }
496
+ if (again !== real) changed();
497
+ // …and that path names the very file that was opened. lstat and open follow every link but the last
498
+ // name's, so a parent folder swapped for a link (to a protected place) and back is caught here.
499
+ let named = null;
500
+ try { named = await fs.stat(real); } catch { named = null; }
501
+ if (!named || named.dev !== opened.dev || named.ino !== opened.ino) changed();
502
+ // Where the system can name the open file itself (Linux), that name is checked against the list too.
503
+ const fdPath = await openFilePath(handle, platform, fs);
504
+ const deniedOpen = fdPath ? fileDenial(fdPath, places) : null;
505
+ if (deniedOpen) forbidden(written, deniedOpen);
506
+ // Read to the end and one byte past what the file should hold: a file that grew (or shrank) is refused.
507
+ const buffer = Buffer.alloc(seen.size + 1);
508
+ let total = 0;
509
+ while (total < buffer.length) {
510
+ const { bytesRead } = await handle.read(buffer, total, buffer.length - total, total);
511
+ if (!bytesRead) break;
512
+ total += bytesRead;
513
+ }
514
+ if (total !== seen.size) changed();
515
+ const after = await handle.stat();
516
+ if (after.size !== opened.size || after.ino !== opened.ino || after.mtimeMs !== opened.mtimeMs) changed();
517
+ return {
518
+ path: real, size: seen.size, fileName, type: plan.type, mimetype: plan.mimetype, caption: plan.caption, fallback: plan.fallback,
519
+ data: buffer.subarray(0, seen.size),
520
+ };
521
+ } finally {
522
+ await handle.close().catch(() => {});
523
+ }
524
+ }