@rubytech/create-sitedesk-code 0.1.518 → 0.1.520

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 (346) hide show
  1. package/dist/__tests__/edge-bind-template-wiring.test.js +37 -0
  2. package/dist/__tests__/samba-provision.test.js +103 -1
  3. package/dist/index.js +19 -4
  4. package/dist/samba-provision.js +72 -0
  5. package/package.json +1 -1
  6. package/payload/platform/docs/superpowers/plans/2026-07-28-task-2097-email-signature-set.md +65 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-28-task-2097-email-signature-set-design.md +155 -0
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +69 -19
  9. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  10. package/payload/platform/plugins/cloudflare/bin/portal-assemble.mjs +187 -0
  11. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +46 -5
  12. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +19 -7
  13. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +154 -38
  14. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +100 -29
  15. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-assemble.test.ts +143 -0
  16. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-delete-withdraw.test.ts +12 -5
  17. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +15 -3
  18. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +105 -11
  19. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +15 -2
  20. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-moved-download.test.ts +9 -5
  21. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-schema-shape.test.ts +47 -5
  22. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +26 -2
  23. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-target.test.ts +12 -4
  24. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-multipart.test.ts +23 -15
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-upload-target.test.ts +23 -15
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-uploads-listing.test.ts +8 -0
  27. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +23 -6
  28. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +36 -20
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +85 -22
  30. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +47 -7
  31. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +27 -18
  32. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +10 -3
  33. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +27 -11
  34. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/target.ts +18 -9
  35. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +16 -0
  36. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +16 -4
  37. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +26 -11
  38. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -10
  39. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +13 -9
  40. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +42 -22
  41. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +17 -4
  42. package/payload/platform/plugins/docs/references/admin-ui.md +43 -5
  43. package/payload/platform/plugins/docs/references/samba.md +2 -0
  44. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  45. package/payload/platform/plugins/email/PLUGIN.md +20 -1
  46. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts +2 -0
  47. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.d.ts.map +1 -0
  48. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js +142 -0
  49. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list-tool.test.js.map +1 -0
  50. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts +2 -0
  51. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.d.ts.map +1 -0
  52. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js +153 -0
  53. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-list.test.js.map +1 -0
  54. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts +2 -0
  55. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.d.ts.map +1 -0
  56. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js +203 -0
  57. package/payload/platform/plugins/email/mcp/dist/__tests__/email-signature-set.test.js.map +1 -0
  58. package/payload/platform/plugins/email/mcp/dist/index.js +69 -0
  59. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  60. package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
  61. package/payload/platform/plugins/email/mcp/dist/lib/compose.js +5 -7
  62. package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
  63. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +42 -0
  64. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  65. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +76 -0
  66. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  67. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +38 -0
  68. package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -1
  69. package/payload/platform/plugins/email/mcp/dist/lib/signature.js +67 -2
  70. package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -1
  71. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts +10 -0
  72. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.d.ts.map +1 -0
  73. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js +73 -0
  74. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-list.js.map +1 -0
  75. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts +18 -0
  76. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.d.ts.map +1 -0
  77. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js +38 -0
  78. package/payload/platform/plugins/email/mcp/dist/tools/email-signature-set.js.map +1 -0
  79. package/payload/platform/plugins/email/references/email-reference.md +14 -2
  80. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +10 -0
  81. package/payload/platform/plugins/email/skills/email-signature/SKILL.md +61 -0
  82. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/index.js +26 -15
  84. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  85. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts +2 -0
  86. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +48 -0
  88. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +24 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -0
  91. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +33 -0
  92. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -0
  93. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +22 -1
  94. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  95. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts +2 -0
  96. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.d.ts.map +1 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js +30 -0
  98. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel-meeting.test.js.map +1 -0
  99. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts +2 -0
  100. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.d.ts.map +1 -0
  101. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js +43 -0
  102. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-get-meeting.test.js.map +1 -0
  103. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts +2 -0
  104. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js +87 -0
  106. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-list.test.js.map +1 -0
  107. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts +2 -0
  108. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.d.ts.map +1 -0
  109. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js +48 -0
  110. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-meeting.test.js.map +1 -0
  111. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
  112. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +14 -1
  113. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
  114. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts +4 -1
  115. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.d.ts.map +1 -1
  116. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js +39 -2
  117. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-get.js.map +1 -1
  118. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts +4 -1
  119. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.d.ts.map +1 -1
  120. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js +50 -1
  121. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-list.js.map +1 -1
  122. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  123. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +43 -1
  124. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  125. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -3
  126. package/payload/platform/scripts/check-no-esm-require.mjs +5 -3
  127. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  128. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  129. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  130. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
  131. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +5 -3
  132. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
  133. package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
  134. package/payload/platform/services/telegram-channel/dist/instructions.js +13 -10
  135. package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
  136. package/payload/platform/services/webchat-channel/dist/instructions.d.ts +6 -2
  137. package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
  138. package/payload/platform/services/webchat-channel/dist/instructions.js +66 -32
  139. package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
  140. package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
  141. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  142. package/payload/platform/services/webchat-channel/dist/notification.js +11 -4
  143. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  144. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  145. package/payload/platform/services/webchat-channel/dist/server.js +30 -14
  146. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  147. package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
  148. package/payload/platform/services/webchat-channel/dist/turn-follow.js +11 -5
  149. package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
  150. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -3
  151. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  152. package/payload/platform/services/whatsapp-channel/dist/notification.js +9 -5
  153. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  154. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  155. package/payload/platform/services/whatsapp-channel/dist/server.js +6 -1
  156. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  157. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  158. package/payload/platform/services/whatsapp-channel/dist/targets.js +8 -2
  159. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  160. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  161. package/payload/platform/templates/systemd/edge.service.template +1 -1
  162. package/payload/server/chunk-PXNY2TAP.js +1980 -0
  163. package/payload/server/chunk-TIIWMF5G.js +4837 -0
  164. package/payload/server/{chunk-S6HYTIU3.js → chunk-UX2FRNO7.js} +72 -2031
  165. package/payload/server/manager-B6OFOXSW.js +61 -0
  166. package/payload/server/maxy-edge.js +5 -3
  167. package/payload/server/package.json +2 -1
  168. package/payload/server/public/activity.html +5 -5
  169. package/payload/server/public/agents.html +4 -4
  170. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js +1 -0
  171. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
  172. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
  173. package/payload/server/public/assets/AdminShell-Djg6ndne.js +2 -0
  174. package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
  175. package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
  176. package/payload/server/public/assets/{activity-BE9gv5xL.js → activity-sqbyX0pX.js} +1 -1
  177. package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
  178. package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
  179. package/payload/server/public/assets/admin-D9Bt47GY.js +1 -0
  180. package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
  181. package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
  182. package/payload/server/public/assets/{agents-B-x0Ai2v.js → agents-BFYM5EDA.js} +1 -1
  183. package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
  184. package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
  185. package/payload/server/public/assets/{browser-ZEQUkPfs.js → browser-DP5GZe4G.js} +1 -1
  186. package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
  187. package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
  188. package/payload/server/public/assets/calendar-BKaemLDG.js +1 -0
  189. package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
  190. package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
  191. package/payload/server/public/assets/chat-DPhT6urD.js +1 -0
  192. package/payload/server/public/assets/chat-DPhT6urD.js.br +1 -0
  193. package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
  194. package/payload/server/public/assets/chevron-left-BSdXvJcH.js +1 -0
  195. package/payload/server/public/assets/chevron-right-DI7vI3IB.js +1 -0
  196. package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
  197. package/payload/server/public/assets/clock-YUTPXZH5.js +1 -0
  198. package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
  199. package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
  200. package/payload/server/public/assets/data-CfjttTvR.js +1 -0
  201. package/payload/server/public/assets/data-CfjttTvR.js.br +2 -0
  202. package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
  203. package/payload/server/public/assets/{file-text-BTV6EdBv.js → file-text-BKK7R-k_.js} +1 -1
  204. package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
  205. package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
  206. package/payload/server/public/assets/{graph-hD_gPbbL.js → graph-FuxYySjK.js} +3 -3
  207. package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
  208. package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
  209. package/payload/server/public/assets/{graph-labels-CY1T19PY.js → graph-labels-Cip6fmKK.js} +1 -1
  210. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
  211. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
  212. package/payload/server/public/assets/{maximize-2-BFV9VKHO.js → maximize-2-D79AhVDo.js} +1 -1
  213. package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
  214. package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
  215. package/payload/server/public/assets/operator-BPAfgFhP.js +1 -0
  216. package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
  217. package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
  218. package/payload/server/public/assets/page--78CbdFs.js +32 -0
  219. package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
  220. package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
  221. package/payload/server/public/assets/page-BqEyS0W4.js +1 -0
  222. package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
  223. package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
  224. package/payload/server/public/assets/{public-D6Tnj5LU.js → public-D1xs3FzD.js} +1 -1
  225. package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
  226. package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
  227. package/payload/server/public/assets/{rotate-ccw-C-RfQUza.js → rotate-ccw-CcU_XD8B.js} +1 -1
  228. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
  229. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
  230. package/payload/server/public/assets/{routines-NIMElUzV.js → routines-CA7D0myr.js} +1 -1
  231. package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
  232. package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
  233. package/payload/server/public/assets/{skills-N0ZlHosx.js → skills-CfVPPSxO.js} +1 -1
  234. package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
  235. package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
  236. package/payload/server/public/assets/tasks-DgieyZqz.js +1 -0
  237. package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
  238. package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
  239. package/payload/server/public/assets/{time-entry-format-jQxlRPoI.js → time-entry-format-IFqinQJj.js} +1 -1
  240. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
  241. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
  242. package/payload/server/public/assets/{triangle-alert-wUS4rpzv.js → triangle-alert-C5vH__sq.js} +1 -1
  243. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
  244. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
  245. package/payload/server/public/assets/{useCopyFeedback-BRix_wIl.js → useCopyFeedback-BeL2gPl6.js} +1 -1
  246. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
  247. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
  248. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +14 -0
  249. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
  250. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
  251. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +1 -0
  252. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
  253. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
  254. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js +2 -0
  255. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
  256. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
  257. package/payload/server/public/assets/{wrench-sDivAjhf.js → wrench-BBEup3hd.js} +1 -1
  258. package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
  259. package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
  260. package/payload/server/public/browser.html +4 -4
  261. package/payload/server/public/calendar.html +7 -7
  262. package/payload/server/public/chat.html +13 -13
  263. package/payload/server/public/data.html +11 -11
  264. package/payload/server/public/graph.html +9 -9
  265. package/payload/server/public/index.html +14 -14
  266. package/payload/server/public/operator.html +14 -14
  267. package/payload/server/public/public.html +13 -13
  268. package/payload/server/public/routines.html +6 -6
  269. package/payload/server/public/skills.html +5 -5
  270. package/payload/server/public/tasks.html +6 -6
  271. package/payload/server/server.js +8176 -11914
  272. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js +0 -1
  273. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.br +0 -0
  274. package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.gz +0 -0
  275. package/payload/server/public/assets/AdminShell-TSa8KgID.js +0 -2
  276. package/payload/server/public/assets/AdminShell-TSa8KgID.js.br +0 -0
  277. package/payload/server/public/assets/AdminShell-TSa8KgID.js.gz +0 -0
  278. package/payload/server/public/assets/activity-BE9gv5xL.js.br +0 -0
  279. package/payload/server/public/assets/activity-BE9gv5xL.js.gz +0 -0
  280. package/payload/server/public/assets/admin-Cb0BZriv.js +0 -1
  281. package/payload/server/public/assets/admin-Cb0BZriv.js.br +0 -0
  282. package/payload/server/public/assets/admin-Cb0BZriv.js.gz +0 -0
  283. package/payload/server/public/assets/agents-B-x0Ai2v.js.br +0 -0
  284. package/payload/server/public/assets/agents-B-x0Ai2v.js.gz +0 -0
  285. package/payload/server/public/assets/browser-ZEQUkPfs.js.br +0 -0
  286. package/payload/server/public/assets/browser-ZEQUkPfs.js.gz +0 -0
  287. package/payload/server/public/assets/calendar-DsRxrW0G.js +0 -1
  288. package/payload/server/public/assets/calendar-DsRxrW0G.js.br +0 -0
  289. package/payload/server/public/assets/calendar-DsRxrW0G.js.gz +0 -0
  290. package/payload/server/public/assets/chat-BS9B9lDo.js +0 -1
  291. package/payload/server/public/assets/chat-BS9B9lDo.js.br +0 -1
  292. package/payload/server/public/assets/chat-BS9B9lDo.js.gz +0 -0
  293. package/payload/server/public/assets/chevron-left-DLtGuH6K.js +0 -1
  294. package/payload/server/public/assets/chevron-right-CY_trg5L.js +0 -1
  295. package/payload/server/public/assets/chevron-right-CY_trg5L.js.br +0 -0
  296. package/payload/server/public/assets/clock-oM5driJm.js +0 -1
  297. package/payload/server/public/assets/clock-oM5driJm.js.br +0 -0
  298. package/payload/server/public/assets/clock-oM5driJm.js.gz +0 -0
  299. package/payload/server/public/assets/data-Cio6PO0F.js +0 -1
  300. package/payload/server/public/assets/data-Cio6PO0F.js.br +0 -2
  301. package/payload/server/public/assets/data-Cio6PO0F.js.gz +0 -0
  302. package/payload/server/public/assets/file-text-BTV6EdBv.js.br +0 -0
  303. package/payload/server/public/assets/file-text-BTV6EdBv.js.gz +0 -0
  304. package/payload/server/public/assets/graph-hD_gPbbL.js.br +0 -0
  305. package/payload/server/public/assets/graph-hD_gPbbL.js.gz +0 -0
  306. package/payload/server/public/assets/graph-labels-CY1T19PY.js.br +0 -0
  307. package/payload/server/public/assets/graph-labels-CY1T19PY.js.gz +0 -0
  308. package/payload/server/public/assets/maximize-2-BFV9VKHO.js.br +0 -0
  309. package/payload/server/public/assets/maximize-2-BFV9VKHO.js.gz +0 -0
  310. package/payload/server/public/assets/operator-Dxze8CwV.js +0 -1
  311. package/payload/server/public/assets/operator-Dxze8CwV.js.br +0 -0
  312. package/payload/server/public/assets/operator-Dxze8CwV.js.gz +0 -0
  313. package/payload/server/public/assets/page-Bi86QF6_.js +0 -32
  314. package/payload/server/public/assets/page-Bi86QF6_.js.br +0 -0
  315. package/payload/server/public/assets/page-Bi86QF6_.js.gz +0 -0
  316. package/payload/server/public/assets/page-BnXly9aN.js +0 -1
  317. package/payload/server/public/assets/page-BnXly9aN.js.br +0 -0
  318. package/payload/server/public/assets/page-BnXly9aN.js.gz +0 -0
  319. package/payload/server/public/assets/public-D6Tnj5LU.js.br +0 -0
  320. package/payload/server/public/assets/public-D6Tnj5LU.js.gz +0 -0
  321. package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.br +0 -0
  322. package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.gz +0 -0
  323. package/payload/server/public/assets/routines-NIMElUzV.js.br +0 -0
  324. package/payload/server/public/assets/routines-NIMElUzV.js.gz +0 -0
  325. package/payload/server/public/assets/skills-N0ZlHosx.js.br +0 -0
  326. package/payload/server/public/assets/skills-N0ZlHosx.js.gz +0 -0
  327. package/payload/server/public/assets/tasks-BsB_0duv.js +0 -1
  328. package/payload/server/public/assets/tasks-BsB_0duv.js.br +0 -0
  329. package/payload/server/public/assets/tasks-BsB_0duv.js.gz +0 -0
  330. package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.br +0 -0
  331. package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.gz +0 -0
  332. package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.br +0 -0
  333. package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.gz +0 -0
  334. package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.br +0 -0
  335. package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.gz +0 -0
  336. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js +0 -14
  337. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.br +0 -0
  338. package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.gz +0 -0
  339. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css +0 -1
  340. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
  341. package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
  342. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js +0 -2
  343. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.br +0 -0
  344. package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.gz +0 -0
  345. package/payload/server/public/assets/wrench-sDivAjhf.js.br +0 -0
  346. package/payload/server/public/assets/wrench-sDivAjhf.js.gz +0 -0
@@ -48,7 +48,7 @@ export async function processUpload(
48
48
  // printed a hardcoded ms=0, which an operator reads as "took no time" rather
49
49
  // than "was never measured".
50
50
  const startedMs = now()
51
- const session = await resolveSession(env.DB, sessionId, startedMs)
51
+ const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
52
52
  if (!session) {
53
53
  log('[data-portal] op=upload owner=none result=denied')
54
54
  return { status: 401, payload: { ok: false, error: 'denied' } }
@@ -63,7 +63,7 @@ export async function processUpload(
63
63
 
64
64
  // The target is resolved BEFORE any R2 call, the same ordering rule the owner
65
65
  // gate follows: a denied caller must never be able to cause a store write.
66
- const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
66
+ const target = await checkTarget(env.DB, env.PORTAL_ACCOUNT_ID, session.folders, targetPath)
67
67
  if (!target.ok) {
68
68
  log(
69
69
  `[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
@@ -76,10 +76,11 @@ export async function processUpload(
76
76
  `targetPath=${q(targetPath)} result=allowed`,
77
77
  )
78
78
 
79
- const key = uploadKey(owner, targetPath, filename)
80
- // The key is derived from the session owner, then re-checked through the same
81
- // gate the other routes use — a filename carrying '..' must not escape.
82
- if (!authorizeKey(owner, key)) {
79
+ const key = uploadKey(env.PORTAL_ACCOUNT_ID, owner, targetPath, filename)
80
+ // The key is derived from this portal's account and the session owner, then
81
+ // re-checked through the same gate the other routes use — a filename carrying
82
+ // '..' must not escape.
83
+ if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
83
84
  log(`[data-portal] op=request uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
84
85
  return { status: 400, payload: { ok: false, error: 'invalid filename' } }
85
86
  }
@@ -143,8 +144,9 @@ export async function processUpload(
143
144
  // request time, which is why this line exists.
144
145
  const objectPresent = (await env.BUCKET.head(key)) !== null
145
146
  const rowPresent =
146
- (await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?').bind(key).first()) !==
147
- null
147
+ (await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ? AND accountId = ?')
148
+ .bind(key, env.PORTAL_ACCOUNT_ID)
149
+ .first()) !== null
148
150
  log(
149
151
  `[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} objectPresent=${objectPresent} rowPresent=${rowPresent}`,
150
152
  )
@@ -158,10 +160,14 @@ async function writeManifestRow(
158
160
  env: PortalEnv,
159
161
  row: { fileId: string; owner: string; filename: string; key: string; size: number; at: string; targetPath: string },
160
162
  ): Promise<void> {
163
+ // The row carries its account and the upsert conflicts on (accountId,
164
+ // objectKey). Conflicting on objectKey alone would be a takeover on the
165
+ // shared store: another client's upload at the same key would seize the
166
+ // existing row, reset its ingested flag, and redirect the file.
161
167
  await env.DB.prepare(
162
- `INSERT INTO manifest (fileId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
163
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
164
- ON CONFLICT (objectKey) DO UPDATE SET
168
+ `INSERT INTO manifest (fileId, accountId, ownerId, filename, objectKey, size, uploadedAt, ingested, targetPath)
169
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
170
+ ON CONFLICT (accountId, objectKey) DO UPDATE SET
165
171
  fileId = excluded.fileId,
166
172
  size = excluded.size,
167
173
  uploadedAt = excluded.uploadedAt,
@@ -170,7 +176,17 @@ async function writeManifestRow(
170
176
  devicePath = '',
171
177
  deleted = 0`,
172
178
  )
173
- .bind(row.fileId, row.owner, row.filename, row.key, row.size, row.at, 0, row.targetPath)
179
+ .bind(
180
+ row.fileId,
181
+ env.PORTAL_ACCOUNT_ID,
182
+ row.owner,
183
+ row.filename,
184
+ row.key,
185
+ row.size,
186
+ row.at,
187
+ 0,
188
+ row.targetPath,
189
+ )
174
190
  .run()
175
191
  }
176
192
 
@@ -192,7 +208,7 @@ export async function processMultipartCreate(
192
208
  now: () => number,
193
209
  ): Promise<Handler> {
194
210
  const startedMs = now()
195
- const session = await resolveSession(env.DB, sessionId, startedMs)
211
+ const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
196
212
  if (!session) {
197
213
  log('[data-portal] op=mp-create owner=none result=denied')
198
214
  return { status: 401, payload: { ok: false, error: 'denied' } }
@@ -215,7 +231,7 @@ export async function processMultipartCreate(
215
231
  }
216
232
  }
217
233
 
218
- const target = await checkTarget(env.DB, session.accountId, session.folders, targetPath)
234
+ const target = await checkTarget(env.DB, env.PORTAL_ACCOUNT_ID, session.folders, targetPath)
219
235
  if (!target.ok) {
220
236
  log(
221
237
  `[data-portal] op=target-check uploadId=${uploadId} owner=${q(owner)} ` +
@@ -224,8 +240,8 @@ export async function processMultipartCreate(
224
240
  return { status: 403, payload: { ok: false, error: 'target not allowed' } }
225
241
  }
226
242
 
227
- const key = uploadKey(owner, targetPath, filename)
228
- if (!authorizeKey(owner, key)) {
243
+ const key = uploadKey(env.PORTAL_ACCOUNT_ID, owner, targetPath, filename)
244
+ if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
229
245
  log(`[data-portal] op=mp-create uploadId=${uploadId} owner=${q(owner)} result=bad-filename`)
230
246
  return { status: 400, payload: { ok: false, error: 'invalid filename' } }
231
247
  }
@@ -250,12 +266,15 @@ export async function processMultipartPart(
250
266
  env: PortalEnv,
251
267
  log: Logger,
252
268
  ): Promise<Handler> {
253
- const session = await resolveSession(env.DB, sessionId, Date.now())
269
+ const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, Date.now())
254
270
  if (!session) {
255
271
  log('[data-portal] op=r2-put owner=none result=denied')
256
272
  return { status: 401, payload: { ok: false, error: 'denied' } }
257
273
  }
258
- if (!authorizeKey(session.ownerId, key)) {
274
+ // The part key round-trips through the client on every call, so the full
275
+ // account and owner prefix is re-checked here rather than trusted from the
276
+ // create step that minted it.
277
+ if (!authorizeKey(env.PORTAL_ACCOUNT_ID, session.ownerId, key)) {
259
278
  log(`[data-portal] op=r2-put owner=${q(session.ownerId)} result=denied reason=not-owner`)
260
279
  return { status: 403, payload: { ok: false, error: 'denied' } }
261
280
  }
@@ -311,14 +330,14 @@ export async function processMultipartComplete(
311
330
  now: () => number,
312
331
  ): Promise<Handler> {
313
332
  const startedMs = now()
314
- const session = await resolveSession(env.DB, sessionId, startedMs)
333
+ const session = await resolveSession(env.DB, sessionId, env.PORTAL_ACCOUNT_ID, startedMs)
315
334
  if (!session) {
316
335
  log('[data-portal] op=mp-complete owner=none result=denied')
317
336
  return { status: 401, payload: { ok: false, error: 'denied' } }
318
337
  }
319
338
  const owner = session.ownerId
320
339
  const uploadId = newId()
321
- if (!authorizeKey(owner, key)) {
340
+ if (!authorizeKey(env.PORTAL_ACCOUNT_ID, owner, key)) {
322
341
  log(`[data-portal] op=mp-complete uploadId=${uploadId} owner=${q(owner)} result=denied reason=not-owner`)
323
342
  return { status: 403, payload: { ok: false, error: 'denied' } }
324
343
  }
@@ -366,8 +385,9 @@ export async function processMultipartComplete(
366
385
 
367
386
  const objectPresent = (await env.BUCKET.head(key)) !== null
368
387
  const rowPresent =
369
- (await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ?').bind(key).first()) !==
370
- null
388
+ (await env.DB.prepare('SELECT fileId FROM manifest WHERE objectKey = ? AND accountId = ?')
389
+ .bind(key, env.PORTAL_ACCOUNT_ID)
390
+ .first()) !== null
371
391
  log(
372
392
  `[data-portal] op=complete uploadId=${uploadId} ms=${now() - startedMs} ` +
373
393
  `objectPresent=${objectPresent} rowPresent=${rowPresent}`,
@@ -1,10 +1,18 @@
1
1
  # Cloudflare Pages config for the data portal (data-portal skill).
2
- # The skill fills the placeholders at assemble time:
2
+ #
3
+ # `bin/portal-assemble.mjs` fills these, and nothing else may: hand-filling them
4
+ # is what shipped a portal whose bucket name and config disagreed (Task 2099).
3
5
  # __PROJECT_NAME__ the Pages project name
4
- # __D1_DATABASE_NAME__ the D1 database name (<accountId>-<purpose>)
5
- # __D1_DATABASE_ID__ the id printed by `wrangler d1 create`
6
- # __R2_BUCKET_NAME__ the R2 bucket name (<accountId>-<purpose>)
6
+ # __D1_DATABASE_NAME__ the ONE shared house database (DATA_PORTAL_DB_NAME)
7
+ # __D1_DATABASE_ID__ its id (DATA_PORTAL_DB_ID)
8
+ # __R2_BUCKET_NAME__ the ONE shared house bucket (DATA_PORTAL_BUCKET)
9
+ # __PORTAL_ACCOUNT_ID__ this portal's own account id
7
10
  # __PORTAL_INSTALL_ORIGIN__ the install's PUBLIC origin (https://<public-tunnel-hostname>), NOT the admin/operator host
11
+ #
12
+ # The first three come from platform/config/cloudflare-house.env and are
13
+ # IDENTICAL in every portal: one store backs all of them. __PORTAL_ACCOUNT_ID__
14
+ # is what differs, and it is the only thing separating this client's rows and
15
+ # objects from another client's.
8
16
  name = "__PROJECT_NAME__"
9
17
  pages_build_output_dir = "."
10
18
  compatibility_date = "2024-01-01"
@@ -31,4 +39,9 @@ bucket_name = "__R2_BUCKET_NAME__"
31
39
  # directory it is handed, so a secret in it is a secret in a repo and one deploy
32
40
  # from being served. Set it with `wrangler pages secret put`.
33
41
  [vars]
42
+ # The tenant scope for every query and every object key. A var, not a secret:
43
+ # it is this client's own account id, not a credential, and the worker needs it
44
+ # on every request. Its partner PORTAL_FETCH_SECRET stays out of this file for
45
+ # the reason given above.
46
+ PORTAL_ACCOUNT_ID = "__PORTAL_ACCOUNT_ID__"
34
47
  PORTAL_INSTALL_ORIGIN = "__PORTAL_INSTALL_ORIGIN__"
@@ -79,7 +79,7 @@ either is a regression.
79
79
 
80
80
  **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
81
81
 
82
- **One delivered bubble per turn when the agent replied.** On the admin `/chat`, a turn that used the reply tool shows exactly one delivered bubble — the reply. A trailing sign-off or recap the agent writes after the reply is treated as narration and is not shown as a second bubble (it collapses like any mid-turn prose). Only a turn that ends **without** using the reply tool has its final text promoted to a delivered bubble, so a genuine no-reply answer is never lost. The tradeoff, accepted by the operator: a turn that acknowledges on the reply tool and then writes the real answer as trailing text will not show that trailing answer the agent is steered to put the answer on the reply tool so this stays rare.
82
+ **One delivered bubble per turn on the admin `/chat`.** The admin webchat has no outbound transport, so the agent has no reply tool there: the text its turn ends with **is** the delivered answer, and it renders as the one prominent prose bubble. Mid-turn prose collapses into the folded run as before, so a turn shows exactly one delivered bubble. A turn that reaches its end with no trailing text delivers nothing and is logged as `op=turn-answer-absent`. Public webchat, WhatsApp and Telegram keep their reply tool, and every historical admin transcript still renders its recorded reply-tool bubbles unchanged.
83
83
 
84
84
  **WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is authorised server-side (`authorizeReplyFileRef`) against the send's own account (the conversation owner) **or** the session's pinned account (`getAccountIdForSession` — the exact scope the DeviceFilePicker used to offer it), so a house/managing-admin session can attach a managed-client file the picker listed. Because a reply sends bytes OUT to an external WhatsApp recipient, any other account, a `public` partition, and any install-wide / non-account path are refused fail-closed; the cross-account admission's authority is the session's pinned account, never the caller-supplied body `accountId` (the own-account admission matches the body `accountId`, which the route already trusts via `isValidAccountId` and the existing-conversation guard). A scope refusal returns a distinct HTTP 403 reason (`file-ref-cross-account` / `file-ref-public` / `file-ref-foreign`) and copy, never "device file not found" (reserved for a genuine resolve/stat miss). Per-ref `[wa-manual-send] op=reply-file-ref … via=own|session|deny reason=<…> ok=<bool>` logs the decision before any send; `via=deny ok=true` is the exfiltration signature that must never appear. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
85
85
 
@@ -757,7 +757,7 @@ authoritative. This section names the surfaces and what backs each.
757
757
  > a read-only reader. The item gates on the flyout handler being wired, exactly
758
758
  > like the admin and lite items. The read-only inbound-channel **Conversations**
759
759
  > reader (WhatsApp, Telegram, public webchat, via the admin-gated
760
- > `/api/whatsapp-reader/conversations` route) remains on `/data`'s Conversations
760
+ > `/api/operator-conversations/conversations` route) remains on `/data`'s Conversations
761
761
  > item and in the admin Sidebar; it is no longer reachable from the operator chat
762
762
  > surface. The operator surface is documented in full in the internal doc
763
763
  > `maxy-code/.docs/operator-dashboard.md`.
@@ -799,7 +799,7 @@ authoritative. This section names the surfaces and what backs each.
799
799
  > count. On the operator **chat** surface the handler is always wired, so the
800
800
  > item is always present and opens the live session switcher (above). On `/data`
801
801
  > the handler is wired only when ≥1 reviewable inbound conversation exists
802
- > (fetched from `/whatsapp-reader/conversations` with the operator's session
802
+ > (fetched from `/operator-conversations/conversations` with the operator's session
803
803
  > key), and selecting it renders the read-only reader in place (the file browser
804
804
  > is swapped for it; Back returns) — so `/data` keeps its hide-when-empty
805
805
  > behaviour. The flyout-open signal `[operator-ui] op=conversations-menu
@@ -813,8 +813,8 @@ authoritative. This section names the surfaces and what backs each.
813
813
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so SiteDesk reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
814
814
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). **Clicking the row's body** (live-dot, title, id, or timestamp) opens that same `/chat?session=<id>` as the chat-launch action. The main area stays a `<div>` (it wraps the inline rename input, which a `<button>` may not contain) carrying `role="button"`, `tabIndex`, an "Open <title> in webchat" aria-label, and Enter/Space activation, styled with `conv-main-clickable` for pointer + hover parity with the channel-row button. The click and keyboard handlers no-op while the row is being renamed, so an in-progress edit is preserved; the action icons and the `⋯` overflow menu are a sibling cluster running their own handlers, so activating them never triggers the row-body open. At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
815
815
  | Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
816
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/whatsapp-reader/conversations` |
817
- | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /whatsapp-reader/directives?sessionId=`, bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. This viewer renders **delivered messages only** (both WhatsApp store rows and Telegram session rows, via `forceDeliveredOnly`) with **no per-viewer toggle** — the former "Messages only" pill is retired; the admin `/chat` composer's own messages-only control is unaffected. The panel — wallpaper included — is **bounded to `--chat-measure` and centred** (`.platform > .wa-reader`), so it reads as a column the same width as `/data`, not an edge-to-edge beige surface. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
816
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/operator-conversations/conversations` |
817
+ | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/session-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /session-reader/directives?sessionId=`, bytes served by `GET /session-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. This viewer renders **delivered messages only** (both WhatsApp store rows and Telegram session rows, via `forceDeliveredOnly`) with **no per-viewer toggle** — the former "Messages only" pill is retired; the admin `/chat` composer's own messages-only control is unaffected. The panel — wallpaper included — is **bounded to `--chat-measure` and centred** (`.platform > .wa-reader`), so it reads as a column the same width as `/data`, not an edge-to-edge beige surface. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/session-reader/stream`, `/session-reader/directives`, `/session-reader/directive` |
818
818
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
819
819
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
820
820
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
@@ -916,6 +916,44 @@ equivalent wording: the reader reply path and the `/send-admin` route.
916
916
  Consolidating them is tracked separately; do not copy their strings into
917
917
  new callers.
918
918
 
919
+ ## WhatsApp inbound notifications
920
+
921
+ An inbound WhatsApp message raises a WhatsApp-green card at the **top right** of
922
+ every admin surface within about a second of arrival. The card shows the
923
+ sub-account it arrived on, the sender (their WhatsApp profile name, or their
924
+ number when they have none), and the first two lines of the message, with an
925
+ ellipsis when there was more. A media message with no caption shows its media
926
+ type instead.
927
+
928
+ The card disappears within five seconds and is **stored nowhere** — there is no
929
+ history, no unread count, and nothing to click. It exists to tell the operator a
930
+ message landed; the reader is where the message itself is read.
931
+
932
+ **Bursts drain rather than pile up.** One card alone lasts five seconds. As more
933
+ arrive, every card on screen speeds up: two share 2.5 seconds each, three 1.7,
934
+ four 1.25, and five or more sit on a 1.2 second floor. A card's dwell only ever
935
+ shortens, so it does not linger once the stack empties. There is no cap on how
936
+ many cards can appear at once.
937
+
938
+ **Four sender types, one icon each:**
939
+
940
+ | Icon | Sender |
941
+ |---|---|
942
+ | Shield | **Admin** — the operator's own number, or a bound admin |
943
+ | Two-way arrows | **Account manager (active)** — a manager whose messages drive an agent turn |
944
+ | Eye | **Account manager (passive)** — a passive bind, which files work but never replies |
945
+ | Person | **Public** — a member of the public |
946
+
947
+ Cards from **every** sub-account appear regardless of which account the tab is
948
+ currently pinned to, and each is labelled with that sub-account's name. That is
949
+ deliberate: seeing another account's message is what tells the operator to
950
+ switch to it.
951
+
952
+ Messages that raise no card, by design: anything the operator sent, group
953
+ messages (groups are observation-only), history catch-up during a re-sync, and
954
+ any sender the access rules decline. A blocked or unrecognised contact is
955
+ silent.
956
+
919
957
  ## Related references
920
958
 
921
959
  - [`platform.md`](platform.md) — UI layout, session reconcile model,
@@ -68,6 +68,8 @@ So:
68
68
 
69
69
  The share is reachable solely from the box itself. Operators reach it by forwarding loopback over SSH — `ssh -L 4445:localhost:445 admin@<tunnel-host>` and then mounting `smb://localhost:4445` — or by joining the box to a private network and re-installing, which makes the private interface the chosen LAN bind.
70
70
 
71
+ That second remedy applies to SMB only. The admin edge uses a different rule: it binds loopback whenever the host carries a public IPv4, whether or not a private interface exists alongside it, so joining a private network does not make the edge LAN-reachable on a cloud box.
72
+
71
73
  If the device has no non-loopback IPv4 interface at all, the installer refuses to provision and exits — there is nothing safe to bind to. This is distinct from the public-only case, which still provisions a working (host-local) share.
72
74
 
73
75
  This classification is the structural guarantee that SMB never leaves the box (loopback-only) or the LAN (LAN-only), even if upstream firewall rules are misconfigured. The Cloudflare tunnel that fronts the admin UI carries HTTPS only; it does not route SMB.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "email",
3
- "description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract.",
3
+ "description": "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it), email-draft-list (read the Drafts folder back and confirm what is actually in it — the only tool that can verify a draft's presence). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-signature-set (install the signature every email-draft then appends automatically — never hand-write a signature into a draft body), email-status, email-otp-extract.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: email
3
- description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-status, email-otp-extract."
3
+ description: "Email — one or more dedicated email mailboxes per account, each addressable by a `mailbox` selector. Retrieval: email-graph-query for stored email-thread history as :ConversationArchive {source:'email'} with :Section chunks (recall, search, 'threads about X', 'what did Y send?'); email-read / email-search for live IMAP inbox previews (with best-effort per-archive section hints from the graph); email-fetch-body for the COMPLETE decoded body of one message (by uid or message_id) when a preview is not enough; email-fetch-attachment for the extracted TEXT of one attachment (PDF via pdftotext, text/* by decode) when data sits inside a file rather than the body. Ingestion (operator-initiated, human-in-the-loop): email-fetch lists new messages, email-ingest writes the operator-approved subset via the conversation-archive pipeline. Sending: email-send (new), email-reply (threaded — resolves the original via IMAP Message-ID search), email-draft (compose to the mailbox Drafts folder, unsent, for operator review; pass messageId for a threaded draft-reply), email-draft-edit (revise a stored draft, located by its Drafts UID or, when Gmail has renumbered it, its draft Message-ID), email-draft-send (dispatch a stored draft by Drafts UID, then remove it), email-draft-list (read the Drafts folder back and confirm what is actually in it — the only tool that can verify a draft's presence). Deleting: email-delete (move named UIDs from email-read/email-search to the mailbox Trash — recoverable, never an expunge). Config: email-provider-info (pre-password host + app-password lookup from an address alone), email-setup, email-signature-set (install the signature every email-draft then appends automatically — never hand-write a signature into a draft body), email-status, email-otp-extract."
4
4
  tools:
5
5
  - name: email-provider-info
6
6
  publicAllowlist: false
@@ -10,6 +10,10 @@ tools:
10
10
  publicAllowlist: false
11
11
  adminAllowlist: false
12
12
  riskClass: write_local
13
+ - name: email-signature-set
14
+ publicAllowlist: false
15
+ adminAllowlist: false
16
+ riskClass: write_local
13
17
  - name: email-read
14
18
  publicAllowlist: false
15
19
  adminAllowlist: false
@@ -42,6 +46,13 @@ tools:
42
46
  publicAllowlist: false
43
47
  adminAllowlist: false
44
48
  riskClass: external
49
+ # adminAllowlist is true here and false on every sibling: this tool exists to
50
+ # be run AGAINST the other email tools' claims, so it is useless if only their
51
+ # callers can reach it. It is a pure read and writes nothing.
52
+ - name: email-draft-list
53
+ publicAllowlist: false
54
+ adminAllowlist: true
55
+ riskClass: read
45
56
  - name: email-search
46
57
  publicAllowlist: false
47
58
  adminAllowlist: false
@@ -101,6 +112,7 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
101
112
 
102
113
  - **Provider lookup:** `email-provider-info` — given just an email address, returns the inferred IMAP/SMTP hosts and the provider's app-password / trusted-application / IMAP-enable requirement, deterministically from the provider map. Call this BEFORE soliciting a password so the requirement is relayed first; most providers reject the normal login password. Returns "unknown" (operator supplies hosts) or "unsupported" (provider blocks IMAP/SMTP, e.g. Tuta).
103
114
  - **Setup:** `email-setup` — collect credentials in plain chat and connect IMAP/SMTP. Hosts are auto-inferred for known providers; supports alias addresses (`agentAddress`). For hosts where the client-visible Drafts folder is not the one flagged `\Drafts`, pass `draftsFolder` to name it explicitly (e.g. `INBOX.Drafts`).
115
+ - **Signature:** `email-signature-set` — install the account's signature into the three files `email-draft` already reads at the account root (`email-signature.txt`, `.html`, `.font`). Plain text and HTML are stored and appended byte-for-byte; `font` is one CSS font-family value that wraps the HTML body and has no plain-text effect. **A part you omit is removed**, so a text-only call clears a stale HTML signature rather than leaving it to keep being appended; pass every part you want to keep on each call. A call with no parts at all is refused rather than treated as "erase everything". Because the append is deterministic and happens on every draft, a signature must never be written into a draft body by hand — that duplicates the stored one and drifts from it. Pass the operator's real signature read from its source of truth; reconstructing markup from memory or from stripped graph text is what put a dead logo URL into a client draft. Account-scoped, no `mailbox` selector: one signature per account.
104
116
  - **Read inbox:** `email-read` — metadata only (UID, sender, subject, date), plus a best-effort `Archive`/`Archive-Section` hint when the IMAP message's `receivedAt` falls inside a stored `:ConversationArchive {source:'email'}` section, plus an `Attachments (N): filename (mime, size); …` footer when the message carries enclosures. Supports pagination (`before_uid`), folders (`inbox`/`sent`), filtering by sender/date/subject. Metadata is derived from IMAP `bodyStructure` — no bytes downloaded.
105
117
  - **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range. Same per-archive hint and attachment footer as `email-read`.
106
118
  - **Delete (move to Trash):** `email-delete` — move the messages named by `uids` (from a prior `email-read`/`email-search`, scoped to a source `folder` of `inbox` or `sent`) into the mailbox's Trash folder. **Recoverable, never an expunge** — the message leaves the inbox and permanent removal is left to the operator's own mail client or the provider's Trash auto-purge; this tool never permanently deletes. Trash is resolved per provider: the `\Trash` special-use folder if advertised, else the first existing of `Trash`, `[Gmail]/Trash`, `Deleted Items`, `Deleted Messages`. `Junk` is never treated as Trash. A mailbox with no resolvable Trash yields a typed error and moves nothing — there is no silent expunge fallback. A `uid` that is already gone is counted not-moved without failing the rest; the result reports `Moved N of M message(s) to <Trash>`. It deletes only the UIDs you name, never by search criteria.
@@ -110,6 +122,12 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
110
122
  - **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
111
123
  - **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. Pass `messageId` to compose a threaded draft-reply: the parent is resolved via IMAP and the draft carries `In-Reply-To`/`References` plus a `Re:` subject from the parent, exactly like `email-reply` (the `subject` argument is then optional and ignored). The success message names the Drafts folder, the APPEND UID, and the draft's own `Message-ID` (`draft id`) so a saved draft is never indistinguishable from a silent no-op and a later edit has a stable handle. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked, unless a per-account `draftsFolder` override is configured (via `email-setup`), which is honoured before special-use discovery for hosts whose client-visible Drafts is a differently-named mailbox. Use when the operator wants to review and send by hand.
112
124
  - **Revise a draft:** `email-draft-edit` — replace a stored draft. The `targetUid` is a fast-path hint; the draft's own `Message-ID` (`draftMessageId`) is the stable identity, because Gmail renumbers a draft's UID when it is touched. The tool tries the UID first, then relocates by a `Message-ID` header search, then composes the replacement from the parameters (same shape as `email-draft`, plus `targetUid`, optional `draftMessageId`, and an optional `messageId` for a threaded revision), APPENDs it with `\Draft`, and deletes the resolved old copy — the replacement is always appended before the old copy is expunged, so a mid-cycle failure never leaves zero copies. When neither identity resolves a live draft, the prior draft is gone: the replacement is saved as a fresh draft (`created`) rather than failing. A single transient transport drop is retried on a fresh connection; auth and other permanent errors surface immediately. Nothing is sent. The confirmation names the folder, the new UID, and the new draft id.
125
+ - **Verify a draft:** `email-draft-list` — read the Drafts folder back and report what is in it. This is the only tool that can confirm a draft's presence: `email-status` reports connectivity and an inbox count, and `email-read`/`email-search` are inbox/sent only. Optional `uid` or `draftMessageId` narrows to one draft, but the plain listing is preferred, because neither handle is stable — Gmail renumbers a draft's Drafts UID when it is touched, and `email-draft-edit` mints a **new** draft `Message-ID` for every revision, so a handle from earlier in the conversation can miss a draft that is present. A narrowing miss therefore never returns bare: it names the miss, says why a stale handle misses, and lists the drafts that *are* in the folder, so "that handle is not here" can never be read as "no draft is here". Lists every message in the resolved folder, not only `\Draft`-flagged ones, because that is what the operator's own mail client shows and a `draftsFolder` override may point at a folder this plugin did not write. A draft id matching two rows is reported as two rows, not collapsed — the edit path leaves a second copy when an expunge is not confirmed. Nothing is written, sent or deleted.
126
+
127
+ **Confirm before asserting.** After `email-draft` or `email-draft-edit`, and whenever anyone reports a draft they cannot see, read the folder with `email-draft-list` before saying the draft is there. The save confirmation is evidence that the write succeeded, not evidence that the draft is in the folder now.
128
+
129
+ **Never state an unverified cause.** When the draft is confirmed present and the customer's client still does not show it, the honest report is exactly that: the draft is on the server, and the client-side reason is unknown. Do not offer a mail-app setting, a sync toggle or a provider option as the cause unless it has been verified. A successful IMAP write to and read of the Drafts folder proves the folder is exposed over IMAP, so provider "show in IMAP" settings are excluded by the evidence rather than being candidates for it.
130
+
113
131
  - **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then relocate it into `\Sent`. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is relocated only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
114
132
  - **The Sent copy is verified, not assumed.** All three send paths read `\Sent` back after SMTP acceptance and search it by Message-ID, then state what is actually there as a fact separate from the send. A folder and UID are named only when they were read back; "no copy was found", "this server advertises no `\Sent` folder", and "the Sent folder could not be checked" are reported as three distinct outcomes, never collapsed. A send whose copy cannot be located is still a successful send. Each send also appends its observed outcome to `config/email-sent-ledger.jsonl`, and the install heartbeat spawns `dist/scripts/sent-copy-sweep.js` hourly to ask `\Sent`, per mailbox, whether each Message-ID sent in the trailing 24 h is actually present (`[email-sent-sweep] op=reconcile …`). Presence is asked per Message-ID rather than by comparing folder totals, because `\Sent` also holds everything the owner sends from other clients. See [`references/email-reference.md`](references/email-reference.md).
115
133
  - **Ingestion — fetch:** `email-fetch` — list new IMAP messages since the stored high-water mark. Metadata + body preview only — including attachment names, mimetypes, and sizes so the operator sees what's attached before approving. No bytes downloaded, no graph writes. Stages the batch in-memory for `email-ingest`.
@@ -136,6 +154,7 @@ Email ingestion is **operator-initiated and human-in-the-loop**. There is no bac
136
154
 
137
155
  | Skill | Purpose |
138
156
  |-------|---------|
157
+ | [email-signature](skills/email-signature/SKILL.md) | Setting or changing the account's email signature. Carries the loader convention (three files at the account root, verbatim text/html, trimmed font), the rule that a signature is installed only via `email-signature-set`, and the rule that the agent never hand-builds one inside a draft body. |
139
158
  | [email-ingest](skills/email-ingest/SKILL.md) | Operator-initiated ingestion. Drives "check my emails" → list via `email-fetch` → summarise → ask the operator per message → write the approved subset via `email-ingest`. |
140
159
  | [email-composition](skills/email-composition/SKILL.md) | Conversational wrapper for drafting replies, new emails, and inbox triage. Orchestrates the sending `email-*` tools with human-in-the-loop approval on every send; draft intent ("leave it in drafts", "don't send yet") routes to `email-draft`, threaded drafts pass `messageId`, and a stored draft is revised via `email-draft-edit` or sent via `email-draft-send`. |
141
160
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-draft-list-tool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-list-tool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-list-tool.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,142 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // The tool's rendering is the unit under test, so the reader is stubbed at the
3
+ // module boundary. A wholesale vi.mock replaces the WHOLE module, so the factory
4
+ // must name every symbol email-draft-list.ts imports from it — MAX_LIMIT as well
5
+ // as listDrafts — or the import lands undefined.
6
+ const { listMock } = vi.hoisted(() => ({ listMock: vi.fn() }));
7
+ vi.mock("../lib/imap.js", () => ({
8
+ listDrafts: listMock,
9
+ MAX_LIMIT: 50,
10
+ }));
11
+ vi.mock("../lib/credentials.js", () => ({
12
+ loadCredentials: vi.fn(async () => ({
13
+ credentials: { config: { email: "m@x.com" }, password: "pw" },
14
+ })),
15
+ }));
16
+ const { emailDraftList } = await import("../tools/email-draft-list.js");
17
+ const run = (params = {}) => emailDraftList({ accountId: "acc-1", ...params });
18
+ const listing = (over) => ({
19
+ folder: "[Gmail]/Drafts",
20
+ mode: "census",
21
+ resolved: true,
22
+ rows: [],
23
+ total: 0,
24
+ ...over,
25
+ });
26
+ const quote = {
27
+ uid: 28316,
28
+ subject: "Quote",
29
+ to: ["jonathan@apexgrange.com"],
30
+ messageId: "<id-283@x>",
31
+ date: new Date("2026-07-28T09:00:00Z"),
32
+ };
33
+ describe("emailDraftList rendering", () => {
34
+ beforeEach(() => vi.clearAllMocks());
35
+ it("renders a census with the folder and every row", async () => {
36
+ listMock.mockResolvedValue(listing({
37
+ total: 2,
38
+ rows: [
39
+ quote,
40
+ { uid: 10, subject: "Older", to: ["a@x.com"], messageId: "<id-10@x>", date: null },
41
+ ],
42
+ }));
43
+ const text = await run();
44
+ expect(text).toContain("[Gmail]/Drafts");
45
+ expect(text).toContain("UID: 28316");
46
+ expect(text).toContain("jonathan@apexgrange.com");
47
+ expect(text).toContain("<id-283@x>");
48
+ expect(text).toContain("UID: 10");
49
+ });
50
+ it("an empty folder says so and is not an error", async () => {
51
+ listMock.mockResolvedValue(listing({ total: 0, rows: [] }));
52
+ const text = await run();
53
+ expect(text).toMatch(/No drafts in \[Gmail\]\/Drafts/);
54
+ });
55
+ it("a missed handle names the miss AND lists what is present", async () => {
56
+ listMock.mockResolvedValue(listing({ mode: "message-id", resolved: false, total: 1, rows: [quote] }));
57
+ const text = await run({ draftMessageId: "stale@x" });
58
+ expect(text).toContain("not found");
59
+ expect(text).toContain("UID: 28316");
60
+ expect(text).toMatch(/change when the draft is edited/);
61
+ });
62
+ it("a missed handle on a genuinely empty folder does not claim drafts exist", async () => {
63
+ listMock.mockResolvedValue(listing({ mode: "uid", resolved: false, total: 0, rows: [] }));
64
+ const text = await run({ uid: 999 });
65
+ expect(text).toContain("not found");
66
+ expect(text).toMatch(/no drafts at all/);
67
+ });
68
+ it("a confirmed hit reports the folder total alongside the draft", async () => {
69
+ listMock.mockResolvedValue(listing({ mode: "uid", resolved: true, total: 3, rows: [quote] }));
70
+ const text = await run({ uid: 28316 });
71
+ expect(text).toMatch(/Confirmed in \[Gmail\]\/Drafts/);
72
+ expect(text).toContain("3 drafts");
73
+ expect(text).toContain("UID: 28316");
74
+ });
75
+ it("names the duplicate-copy state when one draft id matches twice", async () => {
76
+ listMock.mockResolvedValue(listing({
77
+ mode: "message-id",
78
+ resolved: true,
79
+ total: 2,
80
+ rows: [quote, { ...quote, uid: 28317 }],
81
+ }));
82
+ const text = await run({ draftMessageId: "id-283@x" });
83
+ expect(text).toMatch(/2 copies match/);
84
+ expect(text).toMatch(/expunge is not confirmed/);
85
+ });
86
+ it("a miss over a truncated census states its window too", async () => {
87
+ listMock.mockResolvedValue(listing({
88
+ mode: "uid",
89
+ resolved: false,
90
+ total: 90,
91
+ rows: Array.from({ length: 20 }, (_, i) => ({
92
+ uid: i + 1,
93
+ subject: "s",
94
+ to: [],
95
+ messageId: null,
96
+ date: null,
97
+ })),
98
+ }));
99
+ const text = await run({ uid: 999 });
100
+ expect(text).toMatch(/holds 90 drafts; showing the newest 20/);
101
+ });
102
+ it("names both handles when both were tried", async () => {
103
+ listMock.mockResolvedValue(listing({ mode: "uid", resolved: false, total: 1, rows: [quote] }));
104
+ const text = await run({ uid: 999, draftMessageId: "gone@x" });
105
+ expect(text).toContain("UID 999");
106
+ expect(text).toContain("draft id gone@x");
107
+ });
108
+ it("truncation is stated and never mentions next_before_uid", async () => {
109
+ listMock.mockResolvedValue(listing({
110
+ folder: "Drafts",
111
+ total: 90,
112
+ rows: Array.from({ length: 50 }, (_, i) => ({
113
+ uid: i + 1,
114
+ subject: "s",
115
+ to: [],
116
+ messageId: null,
117
+ date: null,
118
+ })),
119
+ }));
120
+ const text = await run({ limit: 500 });
121
+ expect(text).toMatch(/newest 50 of 90/);
122
+ expect(text).not.toContain("next_before_uid");
123
+ });
124
+ it("clamps limit to MAX_LIMIT and floors it at 1", async () => {
125
+ listMock.mockResolvedValue(listing({}));
126
+ await run({ limit: 500 });
127
+ expect(listMock).toHaveBeenCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 50 }));
128
+ await run({ limit: 0 });
129
+ expect(listMock).toHaveBeenLastCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 1 }));
130
+ });
131
+ it("defaults the limit to 20 when none is given", async () => {
132
+ listMock.mockResolvedValue(listing({}));
133
+ await run();
134
+ expect(listMock).toHaveBeenCalledWith(expect.anything(), "pw", expect.objectContaining({ limit: 20 }));
135
+ });
136
+ it("says the listing is flag-agnostic so an operator can reconcile against their client", async () => {
137
+ listMock.mockResolvedValue(listing({ total: 1, rows: [quote] }));
138
+ const text = await run();
139
+ expect(text).toMatch(/not only \\Draft-flagged/);
140
+ });
141
+ });
142
+ //# sourceMappingURL=email-draft-list-tool.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-list-tool.test.js","sourceRoot":"","sources":["../../src/__tests__/email-draft-list-tool.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=email-draft-list.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-draft-list.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/email-draft-list.test.ts"],"names":[],"mappings":""}