@rubytech/create-maxy-code 0.1.563 → 0.1.565

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 (313) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/PLUGIN.md +2 -2
  3. package/payload/platform/plugins/admin/hooks/__tests__/askuserquestion-channel-carrier-gate.test.sh +1 -1
  4. package/payload/platform/plugins/admin/hooks/askuserquestion-channel-carrier-gate.sh +4 -3
  5. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +27 -4
  6. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  7. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +118 -0
  8. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +15 -0
  9. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +25 -7
  10. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +62 -5
  11. package/payload/platform/plugins/docs/references/outlook-guide.md +7 -3
  12. package/payload/platform/plugins/docs/references/telegram-guide.md +19 -0
  13. package/payload/platform/plugins/memory/references/schema-construction.md +1 -1
  14. package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
  15. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +1 -1
  16. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -1
  17. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js +1 -1
  18. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js.map +1 -1
  19. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +1 -1
  20. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -1
  21. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +1 -1
  22. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -1
  23. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +1 -1
  24. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -1
  25. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  26. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  27. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js +51 -1
  28. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js.map +1 -1
  29. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js +1 -1
  30. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-paged.test.js.map +1 -1
  31. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js +1 -0
  32. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js.map +1 -1
  33. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +2 -2
  34. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -1
  35. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +7 -7
  36. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
  37. package/payload/platform/plugins/outlook/mcp/dist/__tests__/has-attachments.test.js +1 -1
  38. package/payload/platform/plugins/outlook/mcp/dist/__tests__/has-attachments.test.js.map +1 -1
  39. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-attachment.test.js +1 -1
  40. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-attachment.test.js.map +1 -1
  41. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-delete.test.js +1 -1
  42. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-delete.test.js.map +1 -1
  43. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-fetch-body.test.js +1 -1
  44. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-fetch-body.test.js.map +1 -1
  45. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js +1 -1
  46. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list-paging.test.js.map +1 -1
  47. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-otp-extract.test.js +1 -1
  48. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-otp-extract.test.js.map +1 -1
  49. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-reply.test.js +159 -6
  50. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-reply.test.js.map +1 -1
  51. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-delegated-ids.test.js +1 -1
  52. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-delegated-ids.test.js.map +1 -1
  53. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +1 -1
  54. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -1
  55. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +1 -1
  56. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -1
  57. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js +1 -0
  58. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js.map +1 -1
  59. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js +1 -1
  60. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js.map +1 -1
  61. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js +2 -1
  62. package/payload/platform/plugins/outlook/mcp/dist/__tests__/write-tools.test.js.map +1 -1
  63. package/payload/platform/plugins/outlook/mcp/dist/index.js +13 -4
  64. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  65. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +9 -0
  66. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
  67. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  68. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +7 -0
  69. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  70. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +16 -5
  71. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  72. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts +5 -0
  73. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  74. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +81 -2
  75. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  76. package/payload/platform/plugins/whatsapp/PLUGIN.md +2 -2
  77. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -2
  78. package/payload/platform/scripts/check-no-esm-require.mjs +10 -0
  79. package/payload/platform/services/claude-session-manager/dist/channel-mcp.d.ts.map +1 -1
  80. package/payload/platform/services/claude-session-manager/dist/channel-mcp.js +9 -3
  81. package/payload/platform/services/claude-session-manager/dist/channel-mcp.js.map +1 -1
  82. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  83. package/payload/platform/services/claude-session-manager/dist/http-server.js +11 -2
  84. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  85. package/payload/platform/services/claude-session-manager/dist/index.js +0 -1
  86. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  88. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -2
  90. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  91. package/payload/platform/services/claude-session-manager/dist/public-allowlist.d.ts +4 -2
  92. package/payload/platform/services/claude-session-manager/dist/public-allowlist.d.ts.map +1 -1
  93. package/payload/platform/services/claude-session-manager/dist/public-allowlist.js +15 -18
  94. package/payload/platform/services/claude-session-manager/dist/public-allowlist.js.map +1 -1
  95. package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts +4 -2
  96. package/payload/platform/services/claude-session-manager/dist/specialist-roster.d.ts.map +1 -1
  97. package/payload/platform/services/claude-session-manager/dist/specialist-roster.js +4 -2
  98. package/payload/platform/services/claude-session-manager/dist/specialist-roster.js.map +1 -1
  99. package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
  100. package/payload/platform/services/telegram-channel/dist/instructions.js +31 -16
  101. package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
  102. package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
  103. package/payload/platform/services/telegram-channel/dist/server.js +21 -38
  104. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  105. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +6 -0
  106. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -1
  107. package/payload/platform/services/telegram-channel/dist/turn-follow.js +16 -1
  108. package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -1
  109. package/payload/platform/services/webchat-channel/dist/instructions.d.ts +0 -2
  110. package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
  111. package/payload/platform/services/webchat-channel/dist/instructions.js +25 -19
  112. package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
  113. package/payload/platform/services/webchat-channel/dist/notification.d.ts +3 -2
  114. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  115. package/payload/platform/services/webchat-channel/dist/notification.js +14 -4
  116. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  117. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  118. package/payload/platform/services/webchat-channel/dist/server.js +16 -48
  119. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  120. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts +6 -0
  121. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  122. package/payload/platform/services/webchat-channel/dist/turn-follow.js +16 -1
  123. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  124. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +5 -2
  125. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  126. package/payload/platform/services/whatsapp-channel/dist/notification.js +31 -14
  127. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  128. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  129. package/payload/platform/services/whatsapp-channel/dist/server.js +64 -161
  130. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  131. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +7 -38
  132. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  133. package/payload/platform/services/whatsapp-channel/dist/targets.js +7 -51
  134. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  135. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +6 -0
  136. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
  137. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +21 -1
  138. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
  139. package/payload/server/{adminuser-self-heal-ZHFNDKJK.js → adminuser-self-heal-Y4U3UNKF.js} +10 -2
  140. package/payload/server/{chunk-G6ULACHG.js → chunk-637C2FGE.js} +3385 -3275
  141. package/payload/server/{chunk-HYQNUVGO.js → chunk-KVR2FT6O.js} +12 -1
  142. package/payload/server/{chunk-SKYZG6OT.js → chunk-PGGGWUBM.js} +372 -224
  143. package/payload/server/{chunk-JOVREC6E.js → chunk-Y4NH2KLF.js} +3789 -3484
  144. package/payload/server/{manager-2VEJKVEO.js → manager-R3XH3BNM.js} +3 -3
  145. package/payload/server/maxy-edge.js +27 -4
  146. package/payload/server/public/activity.html +5 -5
  147. package/payload/server/public/agents.html +4 -4
  148. package/payload/server/public/assets/{AdminLoginScreens-C6e5jf0S.js → AdminLoginScreens-5gI137xF.js} +1 -1
  149. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.br +0 -0
  150. package/payload/server/public/assets/AdminLoginScreens-5gI137xF.js.gz +0 -0
  151. package/payload/server/public/assets/{AdminShell-DStA0lfm.js → AdminShell-Bfapavlp.js} +2 -2
  152. package/payload/server/public/assets/AdminShell-Bfapavlp.js.br +0 -0
  153. package/payload/server/public/assets/AdminShell-Bfapavlp.js.gz +0 -0
  154. package/payload/server/public/assets/{activity--yyBcNfB.js → activity-DnbsmUgn.js} +1 -1
  155. package/payload/server/public/assets/activity-DnbsmUgn.js.br +0 -0
  156. package/payload/server/public/assets/activity-DnbsmUgn.js.gz +0 -0
  157. package/payload/server/public/assets/admin-BXIv0_oj.js +1 -0
  158. package/payload/server/public/assets/admin-BXIv0_oj.js.br +0 -0
  159. package/payload/server/public/assets/admin-BXIv0_oj.js.gz +0 -0
  160. package/payload/server/public/assets/{agents-odyVOb5E.js → agents-Dl2wOcpj.js} +1 -1
  161. package/payload/server/public/assets/agents-Dl2wOcpj.js.br +0 -0
  162. package/payload/server/public/assets/agents-Dl2wOcpj.js.gz +0 -0
  163. package/payload/server/public/assets/{browser-D1K_MoHt.js → browser-D-Uc9ewO.js} +1 -1
  164. package/payload/server/public/assets/browser-D-Uc9ewO.js.br +0 -0
  165. package/payload/server/public/assets/browser-D-Uc9ewO.js.gz +0 -0
  166. package/payload/server/public/assets/{calendar-jqh9U7Ic.js → calendar-Cu3O6Bay.js} +1 -1
  167. package/payload/server/public/assets/calendar-Cu3O6Bay.js.br +0 -0
  168. package/payload/server/public/assets/calendar-Cu3O6Bay.js.gz +0 -0
  169. package/payload/server/public/assets/chat-Bso94e-f.js +1 -0
  170. package/payload/server/public/assets/chat-Bso94e-f.js.br +0 -0
  171. package/payload/server/public/assets/chat-Bso94e-f.js.gz +0 -0
  172. package/payload/server/public/assets/chevron-left-CDwfR9V-.js +1 -0
  173. package/payload/server/public/assets/chevron-left-CDwfR9V-.js.br +0 -0
  174. package/payload/server/public/assets/clock-DDVrNLt9.js +1 -0
  175. package/payload/server/public/assets/clock-DDVrNLt9.js.br +0 -0
  176. package/payload/server/public/assets/clock-DDVrNLt9.js.gz +0 -0
  177. package/payload/server/public/assets/{copy-DkuHwkaB.js → copy-DPBVV8KZ.js} +1 -1
  178. package/payload/server/public/assets/copy-DPBVV8KZ.js.br +0 -0
  179. package/payload/server/public/assets/copy-DPBVV8KZ.js.gz +0 -0
  180. package/payload/server/public/assets/data-Dei8n2EX.js +1 -0
  181. package/payload/server/public/assets/data-Dei8n2EX.js.br +0 -0
  182. package/payload/server/public/assets/data-Dei8n2EX.js.gz +0 -0
  183. package/payload/server/public/assets/{file-text-C6o_NRjj.js → file-text-Dn82O6ke.js} +1 -1
  184. package/payload/server/public/assets/file-text-Dn82O6ke.js.br +0 -0
  185. package/payload/server/public/assets/file-text-Dn82O6ke.js.gz +0 -0
  186. package/payload/server/public/assets/{graph-BN30v5hJ.js → graph-D2J7a7Ll.js} +1 -1
  187. package/payload/server/public/assets/graph-D2J7a7Ll.js.br +0 -0
  188. package/payload/server/public/assets/{graph-BN30v5hJ.js.gz → graph-D2J7a7Ll.js.gz} +0 -0
  189. package/payload/server/public/assets/{graph-labels-DHFyNXQt.js → graph-labels-D3gkY2IH.js} +1 -1
  190. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.br +0 -0
  191. package/payload/server/public/assets/graph-labels-D3gkY2IH.js.gz +0 -0
  192. package/payload/server/public/assets/{operator-DwFYA4Fp.js → operator-Bz_yj1im.js} +1 -1
  193. package/payload/server/public/assets/operator-Bz_yj1im.js.br +0 -0
  194. package/payload/server/public/assets/operator-Bz_yj1im.js.gz +0 -0
  195. package/payload/server/public/assets/page-D6u2xYh7.js +1 -0
  196. package/payload/server/public/assets/page-D6u2xYh7.js.br +0 -0
  197. package/payload/server/public/assets/page-D6u2xYh7.js.gz +0 -0
  198. package/payload/server/public/assets/{page-CEWIP3VF.js → page-mH4nyQ75.js} +2 -2
  199. package/payload/server/public/assets/page-mH4nyQ75.js.br +0 -0
  200. package/payload/server/public/assets/page-mH4nyQ75.js.gz +0 -0
  201. package/payload/server/public/assets/play-ByuChQJJ.js +1 -0
  202. package/payload/server/public/assets/play-ByuChQJJ.js.br +0 -0
  203. package/payload/server/public/assets/play-ByuChQJJ.js.gz +0 -0
  204. package/payload/server/public/assets/{public-Ceb0n7Km.js → public-CtoYVjPR.js} +1 -1
  205. package/payload/server/public/assets/public-CtoYVjPR.js.br +0 -0
  206. package/payload/server/public/assets/public-CtoYVjPR.js.gz +0 -0
  207. package/payload/server/public/assets/{rotate-ccw-CDYdx91V.js → rotate-ccw-BXoIghE3.js} +1 -1
  208. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.br +0 -0
  209. package/payload/server/public/assets/rotate-ccw-BXoIghE3.js.gz +0 -0
  210. package/payload/server/public/assets/{routines-DwNTih0y.js → routines-DXleYpDv.js} +1 -1
  211. package/payload/server/public/assets/routines-DXleYpDv.js.br +0 -0
  212. package/payload/server/public/assets/routines-DXleYpDv.js.gz +0 -0
  213. package/payload/server/public/assets/{skills-TIDCdOLQ.js → skills-CMl38MKH.js} +1 -1
  214. package/payload/server/public/assets/skills-CMl38MKH.js.br +0 -0
  215. package/payload/server/public/assets/skills-CMl38MKH.js.gz +0 -0
  216. package/payload/server/public/assets/{tasks-eTXvaWnd.js → tasks-Cshaf0Md.js} +1 -1
  217. package/payload/server/public/assets/tasks-Cshaf0Md.js.br +0 -0
  218. package/payload/server/public/assets/tasks-Cshaf0Md.js.gz +0 -0
  219. package/payload/server/public/assets/{triangle-alert-JwLSrj17.js → triangle-alert-CS-NXr_9.js} +1 -1
  220. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.br +3 -0
  221. package/payload/server/public/assets/triangle-alert-CS-NXr_9.js.gz +0 -0
  222. package/payload/server/public/assets/{useCopyFeedback-DjTkNw2p.js → useCopyFeedback-pKLfQJNG.js} +1 -1
  223. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.br +0 -0
  224. package/payload/server/public/assets/useCopyFeedback-pKLfQJNG.js.gz +0 -0
  225. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css +1 -0
  226. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.br +0 -0
  227. package/payload/server/public/assets/useMediaQuery-BHFn0-S9.css.gz +0 -0
  228. package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js → useMediaQuery-oDCasnxD.js} +1 -1
  229. package/payload/server/public/assets/useMediaQuery-oDCasnxD.js.br +0 -0
  230. package/payload/server/public/assets/{useMediaQuery-D8VDA0RN.js.gz → useMediaQuery-oDCasnxD.js.gz} +0 -0
  231. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js +2 -0
  232. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.br +0 -0
  233. package/payload/server/public/assets/useVoiceRecorder-EgO1GmIg.js.gz +0 -0
  234. package/payload/server/public/assets/{wrench-DutUiRu7.js → wrench-czES9Rdy.js} +1 -1
  235. package/payload/server/public/assets/wrench-czES9Rdy.js.br +0 -0
  236. package/payload/server/public/assets/wrench-czES9Rdy.js.gz +0 -0
  237. package/payload/server/public/browser.html +4 -4
  238. package/payload/server/public/calendar.html +7 -7
  239. package/payload/server/public/chat.html +13 -13
  240. package/payload/server/public/data.html +11 -11
  241. package/payload/server/public/graph.html +9 -9
  242. package/payload/server/public/index.html +13 -13
  243. package/payload/server/public/operator.html +14 -14
  244. package/payload/server/public/public.html +13 -13
  245. package/payload/server/public/routines.html +7 -7
  246. package/payload/server/public/skills.html +5 -5
  247. package/payload/server/public/tasks.html +6 -6
  248. package/payload/server/server.js +10213 -8397
  249. package/payload/server/{src-MM64CYYZ.js → src-IJCLNFYU.js} +1 -1
  250. package/payload/server/{userprofile-reconcile-TL4JKBPG.js → userprofile-reconcile-PUA657YX.js} +8 -1
  251. package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.br +0 -0
  252. package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.gz +0 -0
  253. package/payload/server/public/assets/AdminShell-DStA0lfm.js.br +0 -0
  254. package/payload/server/public/assets/AdminShell-DStA0lfm.js.gz +0 -0
  255. package/payload/server/public/assets/activity--yyBcNfB.js.br +0 -0
  256. package/payload/server/public/assets/activity--yyBcNfB.js.gz +0 -0
  257. package/payload/server/public/assets/admin-Dr6Jwe7W.js +0 -1
  258. package/payload/server/public/assets/admin-Dr6Jwe7W.js.br +0 -0
  259. package/payload/server/public/assets/admin-Dr6Jwe7W.js.gz +0 -0
  260. package/payload/server/public/assets/agents-odyVOb5E.js.br +0 -0
  261. package/payload/server/public/assets/agents-odyVOb5E.js.gz +0 -0
  262. package/payload/server/public/assets/browser-D1K_MoHt.js.br +0 -0
  263. package/payload/server/public/assets/browser-D1K_MoHt.js.gz +0 -0
  264. package/payload/server/public/assets/calendar-jqh9U7Ic.js.br +0 -0
  265. package/payload/server/public/assets/calendar-jqh9U7Ic.js.gz +0 -0
  266. package/payload/server/public/assets/chat-QQHBdVor.js +0 -1
  267. package/payload/server/public/assets/chat-QQHBdVor.js.br +0 -0
  268. package/payload/server/public/assets/chat-QQHBdVor.js.gz +0 -0
  269. package/payload/server/public/assets/chevron-left-DwTXgKwr.js +0 -1
  270. package/payload/server/public/assets/clock-DOmh9iKX.js +0 -1
  271. package/payload/server/public/assets/clock-DOmh9iKX.js.br +0 -0
  272. package/payload/server/public/assets/copy-DkuHwkaB.js.br +0 -0
  273. package/payload/server/public/assets/copy-DkuHwkaB.js.gz +0 -0
  274. package/payload/server/public/assets/data-DYmTNqrb.js +0 -1
  275. package/payload/server/public/assets/data-DYmTNqrb.js.br +0 -0
  276. package/payload/server/public/assets/data-DYmTNqrb.js.gz +0 -0
  277. package/payload/server/public/assets/file-text-C6o_NRjj.js.br +0 -0
  278. package/payload/server/public/assets/file-text-C6o_NRjj.js.gz +0 -0
  279. package/payload/server/public/assets/graph-BN30v5hJ.js.br +0 -0
  280. package/payload/server/public/assets/graph-labels-DHFyNXQt.js.br +0 -0
  281. package/payload/server/public/assets/graph-labels-DHFyNXQt.js.gz +0 -0
  282. package/payload/server/public/assets/operator-DwFYA4Fp.js.br +0 -0
  283. package/payload/server/public/assets/operator-DwFYA4Fp.js.gz +0 -0
  284. package/payload/server/public/assets/page-BALVRssF.js +0 -1
  285. package/payload/server/public/assets/page-BALVRssF.js.br +0 -0
  286. package/payload/server/public/assets/page-BALVRssF.js.gz +0 -0
  287. package/payload/server/public/assets/page-CEWIP3VF.js.br +0 -0
  288. package/payload/server/public/assets/page-CEWIP3VF.js.gz +0 -0
  289. package/payload/server/public/assets/play-Bg-BY_oj.js +0 -1
  290. package/payload/server/public/assets/play-Bg-BY_oj.js.br +0 -0
  291. package/payload/server/public/assets/play-Bg-BY_oj.js.gz +0 -0
  292. package/payload/server/public/assets/public-Ceb0n7Km.js.br +0 -0
  293. package/payload/server/public/assets/public-Ceb0n7Km.js.gz +0 -0
  294. package/payload/server/public/assets/rotate-ccw-CDYdx91V.js.gz +0 -0
  295. package/payload/server/public/assets/routines-DwNTih0y.js.br +0 -0
  296. package/payload/server/public/assets/routines-DwNTih0y.js.gz +0 -0
  297. package/payload/server/public/assets/skills-TIDCdOLQ.js.br +0 -0
  298. package/payload/server/public/assets/skills-TIDCdOLQ.js.gz +0 -0
  299. package/payload/server/public/assets/tasks-eTXvaWnd.js.br +0 -0
  300. package/payload/server/public/assets/tasks-eTXvaWnd.js.gz +0 -0
  301. package/payload/server/public/assets/triangle-alert-JwLSrj17.js.br +0 -0
  302. package/payload/server/public/assets/triangle-alert-JwLSrj17.js.gz +0 -0
  303. package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.br +0 -0
  304. package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.gz +0 -0
  305. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css +0 -1
  306. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.br +0 -0
  307. package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.gz +0 -0
  308. package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.br +0 -0
  309. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js +0 -2
  310. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.br +0 -0
  311. package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.gz +0 -0
  312. package/payload/server/public/assets/wrench-DutUiRu7.js.br +0 -0
  313. package/payload/server/public/assets/wrench-DutUiRu7.js.gz +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.563",
3
+ "version": "0.1.565",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -274,8 +274,8 @@ Tools are available via the `admin` MCP server.
274
274
  - `hooks/quote-render-pdf-conformance.sh` — **PostToolUse matcher=`mcp__plugin_browser_browser__browser-pdf-save`.** The PDF-time companion to the PreToolUse `quote-render-gate`: the HTML gates run inside `quote-render` before the receipt, but the two PDF-only conformance checks cannot, because the PDF does not exist until `browser-pdf-save` runs. For a gated client-quote PDF (`memory/users/<phone>/documents/quote-<jobId>.pdf` or `…-breakdown.pdf`) whose render receipt names the source HTML, it blocks (exit 2) when the printed PDF's `/URI` annotation count is below the source HTML's absolute-link count (a rasterised or flattened print that dropped links) or when an embedded `https://` link does not answer 200 (a Sign-Online link a pending redeploy would 404). The offline `/URI`-count check runs first and short-circuits, so a link-loss regression never depends on the network. **Block message:** `Blocked: the printed quote PDF is missing links the document declared, or an embedded link is not live (…). Do not send it. If a Sign-Online link is not live yet, deploy the signing page first; then re-render and re-print so the PDF carries every link the document declares.` **Log line** (stderr, on a block): `[quote-render] op=bypass jobId=<jobId> reason=pdf-link-conformance detail=<uri-count …|link <url> -> <code>>`. Every non-quote PDF, and any call with no receipt (the PreToolUse gate owns that block), passes at exit 0. **Fail-open** on an uninspectable call (tty / empty stdin / no `python3` / receipt or PDF absent). **Reach on upgrade:** `provision-account-dir.sh` writes this PostToolUse matcher only at account-provision time, so accounts provisioned before it landed keep a stale matcher and need a settings backfill (tracked as a follow-up task), the same reach limit the AskUserQuestion carrier gate hit.
275
275
  - `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
276
276
  - `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
277
- - `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your channel reply tool and wait for the operator's next message.` **Log line** (stderr, one per decisive call): `[ask-channel] decision=<allow|block> sessionId=<id8> channel=<attached|none> reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>`. **Fail-open** on no envelope, blank session id, or absent channel file — nudges, never bricks the UI. The complementary standing check (`interactive-wedge` in the session manager) catches any un-carried interactive tool_use that slips past this hook; it scans every account's project-slug (not just the boot slug), so a client sub-account channel wedge is covered too. **Reach on upgrade:** `provision-account-dir.sh` writes this matcher only at account-provision time, and the upgrade path re-provisions only the house account — so an account provisioned before this gate landed keeps a stale matcher and re-wedges (live SiteDesk recurrence session `d39a22b0`, account `2078cb54`, 2026-07-15). `setup-account.sh` runs `reconcile_all_accounts_askgate` (`platform/scripts/lib/account-settings-askgate.sh`) on every install to retrofit the carrier gate onto every existing account's `settings.json` — idempotent surgical jq patch of the `AskUserQuestion` matcher, one `[backfill-1683] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
278
- - `hooks/public-tool-surface.sh` — **PreToolUse matcher-less (every tool) + UserPromptSubmit + Stop.** Records the tool surface a `role='public'` spawn REALISES, which nothing recorded before: `tool-allowlist` records the surface a spawn was given, and `[public-surface] op=audit` reconciles MCP server names from spawn-tee filenames, so it is blind to `Read`, `Bash`, `Skill` and `ToolSearch` (no MCP child, no tee file). On 2026-08-01 five such tools ran in a session spawned with `--allowed-tools mcp__whatsapp-channel__reply --strict-mcp-config --permission-mode dontAsk` and produced no line. **Input:** `MAXY_PUBLIC_ALLOWED_TOOLS`, stamped by both spawn paths (`http-server.ts` rc-spawn, `pty-spawner.ts`) for `role='public'` only, carrying that spawn's final `--allowed-tools` argv set comma-joined. Absent means not a public spawn: the hook exits 0 in silence. **Log lines** (server.log only — a visitor's PTY gains no output): `[public-surface] op=tool-realised sessionId=<id8> tool=<name> allowed=<y|n>` per call, `allowed` being exact membership of the stamped set; `[public-surface] op=realised-census sessionId=<id8> calls=<n> disallowed=<n>` at Stop, emitted including zero so a session with no disallowed call is distinguishable from one the hook never saw. Per-call tallies are appends to `<logdir>/public-surface/<sanitised-session-id>`, removed by the census, so no counter mutex is needed. The `UserPromptSubmit` registration is the turn-start reset and emits nothing: the stuck-turn recycler kills a wedged PTY and resumes the SAME session id, so that turn's `Stop` never fires and its tally would otherwise be counted into the next turn's census (measured: three calls in a killed turn plus one call in the next reported `calls=4 disallowed=3`). **Never blocks:** exit 0 on every path, including unparseable stdin, absent `session_id`, missing `python3` and an unwritable log. **Reach on upgrade:** `provision-account-dir.sh` writes both entries only at account-provision time, so `setup-account.sh` runs `reconcile_all_accounts_public_surface` (`platform/scripts/lib/account-settings-public-surface.sh`) on every install to retrofit them onto every existing account, one `[backfill-2339] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
277
+ - `hooks/askuserquestion-channel-carrier-gate.sh` — PreToolUse matcher=`AskUserQuestion`, registered as a **second command on the same matcher** as the investigate-gate (either exit 2 blocks; the two guard different conditions). Blocks the question (exit 2) when the session has a native channel attached, because the Claude Code channel protocol carries only the two permission methods — there is no carrier for the `AskUserQuestion` elicitation, so on an rc-spawn channel (webchat/WhatsApp/Telegram) session the harness would block the turn awaiting a selection that can never arrive and the reader renders only delivered replies, wedging the turn (live SiteDesk incident session `1a085718` 2026-07-11: `/chat` stalled on the delivered "On it…" ack with no card). **Signal:** the rc-spawn writes the channel MCP registration as a config file in `os.tmpdir()` keyed by session id (`maxy-channel-<sid>.json` / `maxy-webchat-channel-<sid>.json` / `maxy-wa-channel-<sid>.json` / `maxy-telegram-channel-<sid>.json`, `sid` sanitized `[^A-Za-z0-9_-]→_` exactly as the writers do); its presence at tool-call time is the authoritative "channel attached to this session" marker. The hook resolves the tmpdir the way Node's `os.tmpdir()` does (`${TMPDIR:-/tmp}`, trailing slash stripped). Correct in both readings of the claude.ai/code scoping question: a Pi channel process sees the tmpfile (block); a claude.ai/code process on another host never sees the Pi tmpfile (fail-open → allow, that surface can answer the card). **Block message:** `Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your answer and wait for the operator's next message.` **Log line** (stderr, one per decisive call): `[ask-channel] decision=<allow|block> sessionId=<id8> channel=<attached|none> reason=<channel-attached|no-channel|fail-open-no-envelope|fail-open-no-session>`. **Fail-open** on no envelope, blank session id, or absent channel file — nudges, never bricks the UI. The complementary standing check (`interactive-wedge` in the session manager) catches any un-carried interactive tool_use that slips past this hook; it scans every account's project-slug (not just the boot slug), so a client sub-account channel wedge is covered too. **Reach on upgrade:** `provision-account-dir.sh` writes this matcher only at account-provision time, and the upgrade path re-provisions only the house account — so an account provisioned before this gate landed keeps a stale matcher and re-wedges (live SiteDesk recurrence session `d39a22b0`, account `2078cb54`, 2026-07-15). `setup-account.sh` runs `reconcile_all_accounts_askgate` (`platform/scripts/lib/account-settings-askgate.sh`) on every install to retrofit the carrier gate onto every existing account's `settings.json` — idempotent surgical jq patch of the `AskUserQuestion` matcher, one `[backfill-1683] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
278
+ - `hooks/public-tool-surface.sh` — **PreToolUse matcher-less (every tool) + UserPromptSubmit + Stop.** Records the tool surface a `role='public'` spawn REALISES, which nothing recorded before: `tool-allowlist` records the surface a spawn was given, and `[public-surface] op=audit` reconciles MCP server names from spawn-tee filenames, so it is blind to `Read`, `Bash`, `Skill` and `ToolSearch` (no MCP child, no tee file). On 2026-08-01 five such tools ran in a session spawned with `--allowed-tools mcp__whatsapp-channel__reply --strict-mcp-config --permission-mode dontAsk` (that reply tool is now retired; a public spawn's allowlist is the deny-basis anchor alone) and produced no line. **Input:** `MAXY_PUBLIC_ALLOWED_TOOLS`, stamped by both spawn paths (`http-server.ts` rc-spawn, `pty-spawner.ts`) for `role='public'` only, carrying that spawn's final `--allowed-tools` argv set comma-joined. Absent means not a public spawn: the hook exits 0 in silence. **Log lines** (server.log only — a visitor's PTY gains no output): `[public-surface] op=tool-realised sessionId=<id8> tool=<name> allowed=<y|n>` per call, `allowed` being exact membership of the stamped set; `[public-surface] op=realised-census sessionId=<id8> calls=<n> disallowed=<n>` at Stop, emitted including zero so a session with no disallowed call is distinguishable from one the hook never saw. Per-call tallies are appends to `<logdir>/public-surface/<sanitised-session-id>`, removed by the census, so no counter mutex is needed. The `UserPromptSubmit` registration is the turn-start reset and emits nothing: the stuck-turn recycler kills a wedged PTY and resumes the SAME session id, so that turn's `Stop` never fires and its tally would otherwise be counted into the next turn's census (measured: three calls in a killed turn plus one call in the next reported `calls=4 disallowed=3`). **Never blocks:** exit 0 on every path, including unparseable stdin, absent `session_id`, missing `python3` and an unwritable log. **Reach on upgrade:** `provision-account-dir.sh` writes both entries only at account-provision time, so `setup-account.sh` runs `reconcile_all_accounts_public_surface` (`platform/scripts/lib/account-settings-public-surface.sh`) on every install to retrofit them onto every existing account, one `[backfill-2339] file=<f> status=<absent|no-hooks|already-set|updated|rewrite-failed>` line per account, contents never echoed.
279
279
  - `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
280
280
  - `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent`.** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone. Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
281
281
  - `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit.** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal this hook was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
@@ -4,7 +4,7 @@
4
4
  # The hook blocks AskUserQuestion when the session has a native channel
5
5
  # attached — signalled by a channel MCP config file for this session id in
6
6
  # tmpdir. A channel carries no elicitation card, so the turn would wedge; the
7
- # hook steers the agent to ask in prose via its channel reply tool instead.
7
+ # hook steers the agent to ask in prose in its own answer instead.
8
8
  #
9
9
  # Covers (per .tasks/1552):
10
10
  # 1. Channel attached (webchat config file present) + AskUserQuestion → BLOCK channel-attached
@@ -10,8 +10,9 @@
10
10
  # wedges — the operator sees the last delivered reply and a live-looking but
11
11
  # dead composer (see .tasks/1552, live SiteDesk incident session 1a085718
12
12
  # 2026-07-11). This hook removes that wedge deterministically by steering the
13
- # agent to ask the same question as prose via its channel reply tool, which
14
- # rides the ordinary delivered-reply path that works on every surface.
13
+ # agent to ask the same question as prose in its answer, which rides the ordinary
14
+ # delivery path that works on every surface. Task 2078 — that path is the turn's
15
+ # own final text now; before it, the channel reply tool.
15
16
  #
16
17
  # This is a SEPARATE concern from askuserquestion-investigate-gate.sh (which
17
18
  # blocks a fabricated menu raised before any investigation). Both are registered
@@ -129,7 +130,7 @@ done
129
130
 
130
131
  if [ "$CHANNEL_ATTACHED" -eq 1 ]; then
131
132
  echo "[ask-channel] decision=block sessionId=${SESSION_ID8} channel=attached reason=channel-attached" >&2
132
- echo "Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your channel reply tool and wait for the operator's next message." >&2
133
+ echo "Blocked: this is a native-channel session and AskUserQuestion cannot be delivered over a channel (the channel protocol carries no elicitation card, so the turn would wedge). Ask the same question as prose in your answer and wait for the operator's next message." >&2
133
134
  exit 2
134
135
  fi
135
136
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:5b93e9b58f687c920da46bec1137f326c5c00d5f2b1d4a460a5f5248eaee0c15
4
+ content-hash: sha256:c0d108d50a5c5109da65ad22f4838284c614aed9161d4b5ab70c6db7c30904c2
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2544,6 +2544,25 @@ Ask Maxy: "What messages has the bot received?" or "Show recent Telegram activit
2544
2544
 
2545
2545
  **Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
2546
2546
 
2547
+ ## Sending from the dashboard
2548
+
2549
+ Open a Telegram conversation in the dashboard and you can type a reply straight
2550
+ into it, the same way you already can with WhatsApp. The message goes out from
2551
+ your bot.
2552
+
2553
+ Two things to know:
2554
+
2555
+ - **Direct messages only.** Group and supergroup chats show no message box. A
2556
+ Telegram conversation is filed under the person who spoke, so a message sent
2557
+ from a group thread would reach that one person privately rather than the
2558
+ group — the box is hidden rather than doing something you did not intend.
2559
+ - **History starts now.** Messages are only shown from the point this feature
2560
+ was installed onward. A conversation that was already running looks empty
2561
+ until the next message arrives, and then fills up from there.
2562
+
2563
+ Text only for now. Photos, files and voice notes are not available on Telegram
2564
+ from the dashboard.
2565
+
2547
2566
  ---
2548
2567
  # Outlook
2549
2568
  Source: https://docs.getmaxy.com/outlook-guide.md
@@ -2570,7 +2589,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2570
2589
  | `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
2571
2590
  | `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
2572
2591
  | `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
2573
- | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
2592
+ | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. A reply whose recipients resolve **only** to the sending mailbox is **refused** before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to`. The resolved recipients are logged as `to=`. Needs `Mail.ReadWrite` + `Mail.Send`. |
2574
2593
  | `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
2575
2594
  | `outlook-mail-ingest` | Write operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads (the analogue of the IMAP `email-ingest`). Input `decisions`: one `{messageId, disposition:'ingest'\|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched, threads group by Graph `conversationId`, and participants resolve closed-set against Neo4j (`:Person`/`:AdminUser` by `accountId`); an unresolved From/To/Cc address skips its whole thread and is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch through the shared `conversation-archive-ingest.sh --source email`. Human-in-the-loop: nothing lands without an explicit per-message decision. The plugin's only Neo4j coupling. Needs `Mail.Read`. |
2576
2595
  | `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
@@ -2587,7 +2606,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2587
2606
  | `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
2588
2607
  | `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
2589
2608
  | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
2590
- | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
2609
+ | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. The result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent and `outlook-draft-edit` can re-address it. |
2591
2610
  | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
2592
2611
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
2593
2612
 
@@ -2657,8 +2676,12 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2657
2676
  | Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
2658
2677
  | Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
2659
2678
  | Send failed | `send-failed account=<id> error=<msg>` |
2679
+ | Reply sent | `mail-reply op=reply account=<id> id=<parentId> convId=<id\|null> replyAll=<bool> attachments=<N> to=<csv>` — `to=` is the recipients Graph resolved; before it, who a sent reply actually went to existed nowhere on disk |
2680
+ | Reply refused (self-addressed) | `mail-reply op=self-reply-refused account=<id> id=<parentId> to=<csv>` — terminal, nothing sent, draft deleted |
2681
+ | Self-reply guard could not run | `mail-reply op=self-reply-guard state=no-address account=<id> id=<parentId>` — the config carries no mailbox address, so the comparison is impossible and the send proceeds. Distinguishes "guard passed" from "guard absent", which a missing line cannot |
2682
+ | Refused, draft not cleaned up | `mail-reply op=self-reply-cleanup-failed account=<id> draftId=<id> error=<msg>` — followed by the refusal; the orphan is visible in Drafts |
2660
2683
  | Draft request | `draft-request account=<id> threaded=<bool>` |
2661
- | Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
2684
+ | Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null> to=<csv>` |
2662
2685
  | Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
2663
2686
  | Draft-send request | `draft-send-request account=<id> id=<draftId>` |
2664
2687
  | Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-05 (0.1.565)
13
+
14
+ - Every channel now delivers the assistant's answer the same way the admin webchat does: the answer is taken from the conversation record and sent. Previously WhatsApp, Telegram and the public webchat asked the assistant to call a send tool, so an answer was suppressed when it called and lost entirely when it did not. Long answers arrive as ordered segments, and an answer meant for you fans out to every channel you have live, naming any that could not be reached.
15
+ - A sub-account conversation no longer sees the main account's specialists. It was being given the house list while dispatch used its own, so the two disagreed.
16
+ - Replying to a message your own mailbox sent no longer emails you instead of the intended recipient, and the reply reports who it is going to before it is sent rather than after.
17
+
18
+ ## 2026-08-05 (0.1.564)
19
+
20
+ - You can now reply to a Telegram conversation from the dashboard. Telegram was read-only, so a conversation you could watch was one you could not answer; it now has a composer like WhatsApp. Sending is restricted to private chats.
21
+ - An arriving Telegram message now raises a card and a desktop notification, the same as WhatsApp. Until now it arrived silently on every admin surface.
22
+ - A portal job sheet now records time, so a day's labour reaches the timesheet. Field work could be marked done but carried no hours and wrote no labour cost at either end.
23
+
12
24
  ## 2026-08-05 (0.1.563)
13
25
 
14
26
  - You can now update from the dashboard. The installed version sits in the sidebar header rather than buried in the footer menu, and when a newer one is available the control there starts the update and shows its progress live. Previously the only route was asking the assistant in chat, and an available update was invisible until you opened the menu.
@@ -1,6 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import {
3
+ MAX_NOTE_LENGTH,
3
4
  MAX_STATUS_LENGTH,
5
+ WORK_DATE_RE,
4
6
  processVisitStatus,
5
7
  } from '../../skills/data-portal/template/functions/api/visit-status'
6
8
  import type {
@@ -174,3 +176,119 @@ describe('the visit-status write gate', () => {
174
176
  expect(joined).not.toContain('granary')
175
177
  })
176
178
  })
179
+
180
+ describe('the working day a status edit may carry', () => {
181
+ const day = (extra: Record<string, unknown>) => ({ visitId: 'v1', status: 'done', ...extra })
182
+
183
+ it('inserts the day, the date and the note when the claim is well formed', async () => {
184
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
185
+ const h = await processVisitStatus(
186
+ day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'second fix' }),
187
+ 'sess',
188
+ env(database),
189
+ () => {},
190
+ 1,
191
+ )
192
+ expect(h.status).toBe(200)
193
+ expect(inserted[0]).toContain(100)
194
+ expect(inserted[0]).toContain('2026-08-05')
195
+ expect(inserted[0]).toContain('second fix')
196
+ })
197
+
198
+ it('defaults to no claim when the body carries no day', async () => {
199
+ // A status-only edit is the common case and must stay a one-field write.
200
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
201
+ const h = await processVisitStatus(day({}), 'sess', env(database), () => {}, 1)
202
+ expect(h.status).toBe(200)
203
+ expect(inserted[0]).toContain(0)
204
+ expect(inserted[0]).toContain('')
205
+ })
206
+
207
+ it('refuses a day outside 0..100 rather than writing it into the graph', async () => {
208
+ // A value above a full day is described by no shipped surface. Refused at
209
+ // the door, exactly as an over-long status is.
210
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
211
+ const e = env(database)
212
+ for (const d of [101, -1, 50.5, '100']) {
213
+ const h = await processVisitStatus(
214
+ day({ dayHundredths: d, workDate: '2026-08-05' }),
215
+ 'sess',
216
+ e,
217
+ () => {},
218
+ 1,
219
+ )
220
+ expect(h.status).toBe(400)
221
+ }
222
+ expect(inserted).toEqual([])
223
+ })
224
+
225
+ it('refuses a claim whose date is missing or malformed', async () => {
226
+ // The date becomes part of the cost key. A claim the roll-up cannot date is
227
+ // told to the person now, not left sitting in the queue.
228
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
229
+ const e = env(database)
230
+ for (const wd of [undefined, '', '5th Aug', '2026-8-5']) {
231
+ const h = await processVisitStatus(
232
+ day({ dayHundredths: 100, workDate: wd }),
233
+ 'sess',
234
+ e,
235
+ () => {},
236
+ 1,
237
+ )
238
+ expect(h.status).toBe(400)
239
+ }
240
+ expect(inserted).toEqual([])
241
+ })
242
+
243
+ it('accepts a date only in the shape the pull will key on', () => {
244
+ expect(WORK_DATE_RE.test('2026-08-05')).toBe(true)
245
+ expect(WORK_DATE_RE.test('2026-08-05T09:00:00Z')).toBe(false)
246
+ })
247
+
248
+ it('refuses a note longer than the cap rather than truncating it', async () => {
249
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
250
+ const h = await processVisitStatus(
251
+ day({
252
+ dayHundredths: 100,
253
+ workDate: '2026-08-05',
254
+ workNote: 'x'.repeat(MAX_NOTE_LENGTH + 1),
255
+ }),
256
+ 'sess',
257
+ env(database),
258
+ () => {},
259
+ 1,
260
+ )
261
+ expect(h.status).toBe(400)
262
+ expect(inserted).toEqual([])
263
+ })
264
+
265
+ it('logs whether the edit carried a day, and never the note', async () => {
266
+ const lines: string[] = []
267
+ const { database } = db(ROWS, person('alice', 'own'))
268
+ await processVisitStatus(
269
+ day({ dayHundredths: 100, workDate: '2026-08-05', workNote: 'granary court rewire' }),
270
+ 'sess',
271
+ env(database),
272
+ (l) => lines.push(l),
273
+ 1,
274
+ )
275
+ const joined = lines.join('\n')
276
+ expect(joined).toContain('labour=true')
277
+ expect(joined).not.toContain('granary')
278
+ expect(joined).not.toContain('2026-08-05')
279
+ })
280
+
281
+ it('still refuses another person visit when the edit carries a day', async () => {
282
+ // The gate is the SELECT and the new fields must not have moved it.
283
+ const { database, inserted } = db(ROWS, person('alice', 'own'))
284
+ const h = await processVisitStatus(
285
+ { visitId: 'v2', status: 'done', dayHundredths: 100, workDate: '2026-08-05' },
286
+ 'sess',
287
+ env(database),
288
+ () => {},
289
+ 1,
290
+ )
291
+ expect(h.status).toBe(404)
292
+ expect(inserted).toEqual([])
293
+ })
294
+ })
@@ -147,6 +147,21 @@ ALTER TABLE people ADD COLUMN recordScope TEXT NOT NULL DEFAULT 'own';
147
147
  No backfill follows it. Everyone already enrolled lands on the narrow scope, which is the intended
148
148
  outcome, and widening one person is a re-run of enrolment for that person.
149
149
 
150
+ **A status edit may also carry a working day, and a store that predates those three columns needs
151
+ them added once too.** `dayHundredths` is integer hundredths of a day, which is what the timesheet
152
+ engine consumes: 100 is a full day, and 0 means the edit claims no labour. `workDate` is the day
153
+ worked, and the pull refuses a claim without one rather than dating it from when the sheet was filed.
154
+ `workNote` is what the worker did. The same `IF NOT EXISTS` rule applies, so run all three:
155
+
156
+ ```sql
157
+ ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
158
+ ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
159
+ ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
160
+ ```
161
+
162
+ Until they exist the endpoint's INSERT names columns the table lacks and fails loudly, which is the
163
+ intended outcome: a status edit silently losing its hours is the failure worth avoiding.
164
+
150
165
  **A passcode is never stored in plaintext** — not in `data-portal.json`, not in a log line, nowhere
151
166
  on the device. The operator conveys it to the person out of band. Rotating a passcode is a re-run of
152
167
  the script for that person; there is no separate editor.
@@ -309,14 +309,32 @@ CREATE TABLE IF NOT EXISTS visits_state (
309
309
  -- is counted by the audit. It is never written under another id and never
310
310
  -- dropped: a dropped edit is a promise broken to the person who made it, and it
311
311
  -- emits nothing at the time it happens.
312
+ -- `dayHundredths`, `workDate` and `workNote` are the working day the edit
313
+ -- carries (Task 2432). `dayHundredths` is integer hundredths of a day, which is
314
+ -- exactly what the timesheet engine consumes: 100 is a full day. 0 means this
315
+ -- edit carries no labour claim, the same absent-sentinel convention `ownerId`
316
+ -- uses above for unbound. The pull writes a labour :JobCost only when it is
317
+ -- above 0, and refuses a claim carrying no `workDate` rather than dating it
318
+ -- from anything else: `statusAt` would record the wrong day for a sheet filed
319
+ -- the next morning, and a visit's scheduled date is not evidence of when it was
320
+ -- attended.
321
+ --
322
+ -- IF NOT EXISTS adds none of the three to a table that predates them. Add them
323
+ -- by hand in that case, in this order:
324
+ -- ALTER TABLE visit_status ADD COLUMN dayHundredths INTEGER NOT NULL DEFAULT 0;
325
+ -- ALTER TABLE visit_status ADD COLUMN workDate TEXT NOT NULL DEFAULT '';
326
+ -- ALTER TABLE visit_status ADD COLUMN workNote TEXT NOT NULL DEFAULT '';
312
327
  CREATE TABLE IF NOT EXISTS visit_status (
313
- id INTEGER PRIMARY KEY AUTOINCREMENT,
314
- accountId TEXT NOT NULL,
315
- visitId TEXT NOT NULL,
316
- ownerId TEXT NOT NULL,
317
- status TEXT NOT NULL,
318
- statusAt TEXT NOT NULL,
319
- claimed INTEGER NOT NULL DEFAULT 0
328
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
329
+ accountId TEXT NOT NULL,
330
+ visitId TEXT NOT NULL,
331
+ ownerId TEXT NOT NULL,
332
+ status TEXT NOT NULL,
333
+ statusAt TEXT NOT NULL,
334
+ dayHundredths INTEGER NOT NULL DEFAULT 0,
335
+ workDate TEXT NOT NULL DEFAULT '',
336
+ workNote TEXT NOT NULL DEFAULT '',
337
+ claimed INTEGER NOT NULL DEFAULT 0
320
338
  );
321
339
 
322
340
  -- The pull selects unclaimed rows for one account every sixty seconds, so the
@@ -12,6 +12,27 @@ import { q } from './_lib/log'
12
12
  */
13
13
  export const MAX_STATUS_LENGTH = 64
14
14
 
15
+ /**
16
+ * The longest work note a person may attach to a day.
17
+ *
18
+ * The note reaches `:JobCost.description` through the device-side pull, so it
19
+ * is graph content arriving from the open web and gets the same treatment the
20
+ * status gets: refused at the door, never truncated. The size is chosen for a
21
+ * description cell on the A4 timesheet row, not derived from anything.
22
+ */
23
+ export const MAX_NOTE_LENGTH = 500
24
+
25
+ /**
26
+ * The only date shape a claim may carry.
27
+ *
28
+ * The date becomes part of the cost key the pull merges on, so a free-form
29
+ * string would produce a key nobody can reproduce. A COPY of this check lives
30
+ * in `platform/ui/server/portal-visits-pull.ts` as the invariant at the writer;
31
+ * the two cannot share a constant because this file ships to Cloudflare Pages
32
+ * and cannot import from the platform tree.
33
+ */
34
+ export const WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
35
+
15
36
  /**
16
37
  * Record a status a person set against one of their visits.
17
38
  *
@@ -56,10 +77,39 @@ export async function processVisitStatus(
56
77
  return { status: 400, payload: { ok: false, error: 'status too long' } }
57
78
  }
58
79
 
80
+ // The working day the edit carries, if any. Absent means a status-only edit,
81
+ // which is the common case and stays a zero-claim write.
82
+ const dayHundredths = b.dayHundredths === undefined ? 0 : b.dayHundredths
83
+ if (
84
+ typeof dayHundredths !== 'number' ||
85
+ !Number.isInteger(dayHundredths) ||
86
+ dayHundredths < 0 ||
87
+ dayHundredths > 100
88
+ ) {
89
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=bad-day`)
90
+ return { status: 400, payload: { ok: false, error: 'dayHundredths must be an integer 0..100' } }
91
+ }
92
+ const workDate = typeof b.workDate === 'string' ? b.workDate : ''
93
+ const workNote = typeof b.workNote === 'string' ? b.workNote : ''
94
+ // A claim the roll-up could not date is refused now rather than queued. The
95
+ // pull holds the same invariant, so a row reaching the table another way is
96
+ // still never written under a guessed date.
97
+ if (dayHundredths > 0 && !WORK_DATE_RE.test(workDate)) {
98
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=undated-claim`)
99
+ return {
100
+ status: 400,
101
+ payload: { ok: false, error: 'a day claim requires workDate as YYYY-MM-DD' },
102
+ }
103
+ }
104
+ if (workNote.length > MAX_NOTE_LENGTH) {
105
+ log(`[data-portal] op=visit-status owner=${q(session.ownerId)} result=note-too-long`)
106
+ return { status: 400, payload: { ok: false, error: 'note too long' } }
107
+ }
108
+
59
109
  const scoped = session.recordScope === 'own'
60
110
  const sql =
61
- `INSERT INTO visit_status (accountId, ownerId, status, statusAt, claimed, visitId)
62
- SELECT ?, ?, ?, ?, 0, v.visitId
111
+ `INSERT INTO visit_status (accountId, ownerId, status, statusAt, dayHundredths, workDate, workNote, claimed, visitId)
112
+ SELECT ?, ?, ?, ?, ?, ?, ?, 0, v.visitId
63
113
  FROM visits v
64
114
  JOIN visits_state s
65
115
  ON s.accountId = v.accountId AND s.currentGeneration = v.generation
@@ -74,6 +124,9 @@ export async function processVisitStatus(
74
124
  session.ownerId,
75
125
  status,
76
126
  statusAt,
127
+ dayHundredths,
128
+ workDate,
129
+ workNote,
77
130
  env.PORTAL_ACCOUNT_ID,
78
131
  session.ownerId,
79
132
  visitId,
@@ -83,6 +136,9 @@ export async function processVisitStatus(
83
136
  session.ownerId,
84
137
  status,
85
138
  statusAt,
139
+ dayHundredths,
140
+ workDate,
141
+ workNote,
86
142
  env.PORTAL_ACCOUNT_ID,
87
143
  visitId,
88
144
  )
@@ -100,11 +156,12 @@ export async function processVisitStatus(
100
156
  return { status: 404, payload: { ok: false, error: 'not found' } }
101
157
  }
102
158
 
103
- // Counts, the scope and the outcome. Never the status text: it describes what
104
- // a real person was doing.
159
+ // Counts, the scope, whether a day was claimed, and the outcome. Never the
160
+ // status text, the date or the note: they describe what a real person was
161
+ // doing and when.
105
162
  log(
106
163
  `[data-portal] op=visit-status owner=${q(session.ownerId)} account=${q(session.accountId)} ` +
107
- `scope=${session.recordScope} queued=${changes} result=ok`,
164
+ `scope=${session.recordScope} labour=${dayHundredths > 0} queued=${changes} result=ok`,
108
165
  )
109
166
  return { status: 200, payload: { ok: true, queued: changes } }
110
167
  }
@@ -20,7 +20,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
20
20
  | `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
21
21
  | `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
22
22
  | `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
23
- | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
23
+ | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. A reply whose recipients resolve **only** to the sending mailbox is **refused** before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to`. The resolved recipients are logged as `to=`. Needs `Mail.ReadWrite` + `Mail.Send`. |
24
24
  | `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
25
25
  | `outlook-mail-ingest` | Write operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads (the analogue of the IMAP `email-ingest`). Input `decisions`: one `{messageId, disposition:'ingest'\|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched, threads group by Graph `conversationId`, and participants resolve closed-set against Neo4j (`:Person`/`:AdminUser` by `accountId`); an unresolved From/To/Cc address skips its whole thread and is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch through the shared `conversation-archive-ingest.sh --source email`. Human-in-the-loop: nothing lands without an explicit per-message decision. The plugin's only Neo4j coupling. Needs `Mail.Read`. |
26
26
  | `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
@@ -37,7 +37,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
37
37
  | `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
38
38
  | `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
39
39
  | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
40
- | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
40
+ | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. The result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent and `outlook-draft-edit` can re-address it. |
41
41
  | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
42
42
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
43
43
 
@@ -107,8 +107,12 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
107
107
  | Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
108
108
  | Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
109
109
  | Send failed | `send-failed account=<id> error=<msg>` |
110
+ | Reply sent | `mail-reply op=reply account=<id> id=<parentId> convId=<id\|null> replyAll=<bool> attachments=<N> to=<csv>` — `to=` is the recipients Graph resolved; before it, who a sent reply actually went to existed nowhere on disk |
111
+ | Reply refused (self-addressed) | `mail-reply op=self-reply-refused account=<id> id=<parentId> to=<csv>` — terminal, nothing sent, draft deleted |
112
+ | Self-reply guard could not run | `mail-reply op=self-reply-guard state=no-address account=<id> id=<parentId>` — the config carries no mailbox address, so the comparison is impossible and the send proceeds. Distinguishes "guard passed" from "guard absent", which a missing line cannot |
113
+ | Refused, draft not cleaned up | `mail-reply op=self-reply-cleanup-failed account=<id> draftId=<id> error=<msg>` — followed by the refusal; the orphan is visible in Drafts |
110
114
  | Draft request | `draft-request account=<id> threaded=<bool>` |
111
- | Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
115
+ | Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null> to=<csv>` |
112
116
  | Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
113
117
  | Draft-send request | `draft-send-request account=<id> id=<draftId>` |
114
118
  | Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
@@ -56,3 +56,22 @@ Ask {{productName}}: "What messages has the bot received?" or "Show recent Teleg
56
56
  **Can't send to a group:** The bot must be a member of the group. Add it via the group settings in Telegram, then try again.
57
57
 
58
58
  **Messages not arriving:** Make sure the bot hasn't been blocked. Try sending `/start` to the bot directly.
59
+
60
+ ## Sending from the dashboard
61
+
62
+ Open a Telegram conversation in the dashboard and you can type a reply straight
63
+ into it, the same way you already can with WhatsApp. The message goes out from
64
+ your bot.
65
+
66
+ Two things to know:
67
+
68
+ - **Direct messages only.** Group and supergroup chats show no message box. A
69
+ Telegram conversation is filed under the person who spoke, so a message sent
70
+ from a group thread would reach that one person privately rather than the
71
+ group — the box is hidden rather than doing something you did not intend.
72
+ - **History starts now.** Messages are only shown from the point this feature
73
+ was installed onward. A conversation that was already running looks empty
74
+ until the next message arrives, and then fills up from there.
75
+
76
+ Text only for now. Photos, files and voice notes are not available on Telegram
77
+ from the dashboard.
@@ -116,7 +116,7 @@ MERGE keys per label:
116
116
  - `:Contact` — `(accountId, contactId)` — write-time MERGE
117
117
  - `:PurchaseOrderLine` — `(accountId, poNumber, lineId)` — child of `:PurchaseOrder`
118
118
  - `:Note` — `(accountId, noteId)` — child of `:Job` via `(:Note)-[:ABOUT]->(:Job)`, append-only CREATE (a fresh `noteId` per note, not a content MERGE), so a second note never overwrites the first
119
- - `:Engineer` — `(accountId, personId)` — additional label on `:Person`, set via `memory-update`
119
+ - `:Engineer` — `(accountId, engineerId)` — additional label on `:Person`, set via `memory-update`
120
120
  - `:Supplier` — `(accountId, organizationId)` — additional label on `:Organization`
121
121
 
122
122
  ---
@@ -133,10 +133,10 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
133
133
  - **Multiple mailboxes:** one account can hold several mailboxes. Every operational tool takes an optional `mailbox` argument (email or graphUserId). With one mailbox, omit it. With two or more, name it — omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows them all.
134
134
  - **Mail (read):** `outlook-mail-list` returns messages newest-first with an optional `since`/`before` date window, and `outlook-mail-search` runs a Graph `$search` (KQL) query where `from:`/`to:`/`subject:`/`received>=` filters live — both return Microsoft's short `bodyPreview` (~255 chars) for cheap triage and a `nextCursor` for paging beyond the 250-per-page window (the cursor is an opaque `@odata.nextLink`; pass it back as `cursor`). Graph forbids `$search`+`$filter`, so the strict date window is on the list tool and sender/subject filtering is on the search tool. `outlook-mail-fetch-body` reads the COMPLETE body of one message by id (HTML decoded to text, no preview cap) — the full-read path. All three read tools report `hasAttachments` per message, so the agent knows to reach for the attachment tool.
135
135
  - **Mail (attachments):** `outlook-mail-attachment` lists or downloads any message's attachments — received or draft. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes); a draft is a message in the same collection, so passing a draftId lists what that draft carries, and the returned `name`/`id` is the handle `outlook-draft-edit`'s `removeAttachments` takes. Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
136
- - **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
136
+ - **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. A reply whose recipients resolve **only** to the sending mailbox is refused before the send and its draft deleted: Graph addresses a reply to the original message's sender, so replying to your own sent message addresses it to yourself. An explicit `cc` does not exempt it — use `outlook-mail-send` with an explicit `to` instead. Both `outlook-mail-reply` and `outlook-draft` log the resolved recipients as `to=`. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
137
137
  - **Mail (ingest to graph):** `outlook-mail-ingest` writes operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads, the analogue of the IMAP `email-ingest`. Input is `decisions`: one `{messageId, disposition:'ingest'|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched via `runMailFetchBody`, threads are grouped by Graph `conversationId`, and participant resolution is closed-set — every From/To/Cc address must already resolve to a `:Person.email` or `:AdminUser.email` for the `accountId`, or its thread is skipped and the address is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch one at a time through the shared `plugins/memory/bin/conversation-archive-ingest.sh --source email` (same RFC822 thread-JSON shape the `email` normaliser consumes). This is the plugin's only Neo4j coupling (`neo4j-driver`). Human-in-the-loop: nothing enters the graph without an explicit per-message decision; the `outlook-mail-ingest` skill drives the list → summarise → ask → ingest loop. Requires `Mail.Read`.
138
138
  - **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
139
- - **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
139
+ - **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. The draft result carries `to`, the recipients the draft holds, so the self-addressed case above is visible without fetching the draft back; on this path it is reported rather than refused, because a draft is not sent. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
140
140
  - **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
141
141
  - **Contacts (read-only):** `outlook-contacts-list` returns contacts.
142
142
  - **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.
@@ -10,7 +10,7 @@ function configWithToken(accountId) {
10
10
  const dir = mkdtempSync(join(tmpdir(), "outlook-attach-"));
11
11
  const tokenStore = new TokenStore(accountId, dir);
12
12
  tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
13
- return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
13
+ return { clientId: "test-client", tenantId: "common", accountId, tokenStore, mailboxAddress: null };
14
14
  }
15
15
  const UPLOAD_URL = "https://upload.example.com/session/attach";
16
16
  function mockRouted() {