@rubytech/create-sitedesk-code 0.1.520 → 0.1.522

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 (283) hide show
  1. package/dist/index.js +17 -3
  2. package/package.json +1 -1
  3. package/payload/platform/lib/account-schema-regions/package-lock.json +12 -0
  4. package/payload/platform/lib/active-rules/package-lock.json +12 -0
  5. package/payload/platform/lib/mcp-lifeline/package-lock.json +12 -0
  6. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +46 -7
  7. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  8. package/payload/platform/plugins/docs/references/admin-ui.md +11 -1
  9. package/payload/platform/plugins/docs/references/internals.md +4 -0
  10. package/payload/platform/plugins/docs/references/outlook-guide.md +27 -4
  11. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +46 -1
  12. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -1
  13. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +14 -0
  14. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  15. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +35 -4
  16. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  17. package/payload/platform/plugins/email/references/email-reference.md +1 -1
  18. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  19. package/payload/platform/plugins/outlook/PLUGIN.md +9 -1
  20. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-resolve.test.d.ts +2 -0
  21. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-resolve.test.d.ts.map +1 -0
  22. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-resolve.test.js +151 -0
  23. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-resolve.test.js.map +1 -0
  24. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-store.test.d.ts +2 -0
  25. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-store.test.d.ts.map +1 -0
  26. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-store.test.js +151 -0
  27. package/payload/platform/plugins/outlook/mcp/dist/__tests__/delegated-store.test.js.map +1 -0
  28. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +38 -4
  29. package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
  30. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.d.ts +2 -0
  31. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js +146 -0
  33. package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-prefix-wiring.test.js.map +1 -0
  34. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list-delegated.test.d.ts +2 -0
  35. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list-delegated.test.d.ts.map +1 -0
  36. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list-delegated.test.js +120 -0
  37. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list-delegated.test.js.map +1 -0
  38. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js +8 -8
  39. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js.map +1 -1
  40. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-path-prefix.test.d.ts +2 -0
  41. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-path-prefix.test.d.ts.map +1 -0
  42. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-path-prefix.test.js +27 -0
  43. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-path-prefix.test.js.map +1 -0
  44. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.d.ts +2 -0
  45. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.d.ts.map +1 -0
  46. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js +70 -0
  47. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mail-only-guard.test.js.map +1 -0
  48. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.d.ts +2 -0
  49. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.d.ts.map +1 -0
  50. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js +152 -0
  51. package/payload/platform/plugins/outlook/mcp/dist/__tests__/shared-mailbox-add.test.js.map +1 -0
  52. package/payload/platform/plugins/outlook/mcp/dist/auth/delegated-store.d.ts +57 -0
  53. package/payload/platform/plugins/outlook/mcp/dist/auth/delegated-store.d.ts.map +1 -0
  54. package/payload/platform/plugins/outlook/mcp/dist/auth/delegated-store.js +163 -0
  55. package/payload/platform/plugins/outlook/mcp/dist/auth/delegated-store.js.map +1 -0
  56. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +12 -0
  57. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
  58. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +21 -1
  59. package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
  60. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts +35 -4
  61. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts.map +1 -1
  62. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js +92 -10
  63. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js.map +1 -1
  64. package/payload/platform/plugins/outlook/mcp/dist/index.js +106 -3
  65. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  66. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +49 -0
  67. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
  68. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +126 -8
  69. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  70. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.d.ts +13 -0
  71. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.d.ts.map +1 -1
  72. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.js +9 -0
  73. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.js.map +1 -1
  74. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts +42 -8
  75. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts.map +1 -1
  76. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js +59 -19
  77. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js.map +1 -1
  78. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-add.d.ts +47 -0
  79. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-add.d.ts.map +1 -0
  80. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-add.js +67 -0
  81. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-add.js.map +1 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-remove.d.ts +12 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-remove.d.ts.map +1 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-remove.js +18 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/tools/shared-mailbox-remove.js.map +1 -0
  86. package/payload/platform/plugins/outlook/references/auth.md +15 -2
  87. package/payload/platform/plugins/outlook/references/graph-surfaces.md +8 -0
  88. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +21 -1
  89. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
  90. package/payload/platform/scripts/check-architecture-skill-no-drift.mjs +20 -6
  91. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  92. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  93. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  94. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +17 -5
  95. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  96. package/payload/platform/services/whatsapp-channel/dist/notification.js +30 -6
  97. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  98. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  99. package/payload/platform/services/whatsapp-channel/dist/server.js +53 -21
  100. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  101. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +16 -0
  102. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
  103. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +41 -2
  104. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
  105. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  106. package/payload/server/{chunk-TIIWMF5G.js → chunk-K7CJGZPU.js} +459 -196
  107. package/payload/server/{manager-B6OFOXSW.js → manager-RKL7JZ4Y.js} +3 -1
  108. package/payload/server/public/activity.html +5 -5
  109. package/payload/server/public/agents.html +4 -4
  110. package/payload/server/public/assets/{AdminLoginScreens-SW7mEMcP.js → AdminLoginScreens-zc2_qqh-.js} +1 -1
  111. package/payload/server/public/assets/AdminLoginScreens-zc2_qqh-.js.br +0 -0
  112. package/payload/server/public/assets/AdminLoginScreens-zc2_qqh-.js.gz +0 -0
  113. package/payload/server/public/assets/AdminShell-IGUdTx5q.js +2 -0
  114. package/payload/server/public/assets/AdminShell-IGUdTx5q.js.br +0 -0
  115. package/payload/server/public/assets/AdminShell-IGUdTx5q.js.gz +0 -0
  116. package/payload/server/public/assets/{activity-sqbyX0pX.js → activity-DVgO8YuM.js} +1 -1
  117. package/payload/server/public/assets/activity-DVgO8YuM.js.br +0 -0
  118. package/payload/server/public/assets/activity-DVgO8YuM.js.gz +0 -0
  119. package/payload/server/public/assets/admin-xtpM7fgl.js +1 -0
  120. package/payload/server/public/assets/admin-xtpM7fgl.js.br +0 -0
  121. package/payload/server/public/assets/admin-xtpM7fgl.js.gz +0 -0
  122. package/payload/server/public/assets/{agents-BFYM5EDA.js → agents-P0nTIxo-.js} +1 -1
  123. package/payload/server/public/assets/agents-P0nTIxo-.js.br +0 -0
  124. package/payload/server/public/assets/agents-P0nTIxo-.js.gz +0 -0
  125. package/payload/server/public/assets/{browser-DP5GZe4G.js → browser-Mw7DY3ft.js} +1 -1
  126. package/payload/server/public/assets/browser-Mw7DY3ft.js.br +0 -0
  127. package/payload/server/public/assets/browser-Mw7DY3ft.js.gz +0 -0
  128. package/payload/server/public/assets/{calendar-BKaemLDG.js → calendar-B9gvL_1_.js} +1 -1
  129. package/payload/server/public/assets/calendar-B9gvL_1_.js.br +0 -0
  130. package/payload/server/public/assets/calendar-B9gvL_1_.js.gz +0 -0
  131. package/payload/server/public/assets/chat-DBGqh4wg.js +1 -0
  132. package/payload/server/public/assets/chat-DBGqh4wg.js.br +2 -0
  133. package/payload/server/public/assets/chat-DBGqh4wg.js.gz +0 -0
  134. package/payload/server/public/assets/chevron-left-Bl4vpOM0.js +1 -0
  135. package/payload/server/public/assets/chevron-left-Bl4vpOM0.js.br +0 -0
  136. package/payload/server/public/assets/chevron-right-BExigCnp.js +1 -0
  137. package/payload/server/public/assets/chevron-right-BExigCnp.js.br +0 -0
  138. package/payload/server/public/assets/clock-D8TJvgJC.js +1 -0
  139. package/payload/server/public/assets/clock-D8TJvgJC.js.br +0 -0
  140. package/payload/server/public/assets/data-HfFSxaC2.js +1 -0
  141. package/payload/server/public/assets/data-HfFSxaC2.js.br +0 -0
  142. package/payload/server/public/assets/data-HfFSxaC2.js.gz +0 -0
  143. package/payload/server/public/assets/{file-text-BKK7R-k_.js → file-text-dYBnXNEX.js} +1 -1
  144. package/payload/server/public/assets/file-text-dYBnXNEX.js.br +0 -0
  145. package/payload/server/public/assets/file-text-dYBnXNEX.js.gz +0 -0
  146. package/payload/server/public/assets/{graph-FuxYySjK.js → graph-CDyzd1Et.js} +2 -2
  147. package/payload/server/public/assets/graph-CDyzd1Et.js.br +0 -0
  148. package/payload/server/public/assets/graph-CDyzd1Et.js.gz +0 -0
  149. package/payload/server/public/assets/{graph-labels-Cip6fmKK.js → graph-labels-D1LkKiJr.js} +1 -1
  150. package/payload/server/public/assets/graph-labels-D1LkKiJr.js.br +0 -0
  151. package/payload/server/public/assets/graph-labels-D1LkKiJr.js.gz +0 -0
  152. package/payload/server/public/assets/{maximize-2-D79AhVDo.js → maximize-2-M-iSaznd.js} +1 -1
  153. package/payload/server/public/assets/maximize-2-M-iSaznd.js.br +0 -0
  154. package/payload/server/public/assets/maximize-2-M-iSaznd.js.gz +0 -0
  155. package/payload/server/public/assets/{operator-BPAfgFhP.js → operator-9jfCv1Fg.js} +1 -1
  156. package/payload/server/public/assets/operator-9jfCv1Fg.js.br +0 -0
  157. package/payload/server/public/assets/operator-9jfCv1Fg.js.gz +0 -0
  158. package/payload/server/public/assets/page-CZ-73FAk.js +32 -0
  159. package/payload/server/public/assets/page-CZ-73FAk.js.br +0 -0
  160. package/payload/server/public/assets/page-CZ-73FAk.js.gz +0 -0
  161. package/payload/server/public/assets/{page-BqEyS0W4.js → page-CjwSt7kx.js} +1 -1
  162. package/payload/server/public/assets/page-CjwSt7kx.js.br +0 -0
  163. package/payload/server/public/assets/page-CjwSt7kx.js.gz +0 -0
  164. package/payload/server/public/assets/{public-D1xs3FzD.js → public-CN-ltC8x.js} +1 -1
  165. package/payload/server/public/assets/public-CN-ltC8x.js.br +0 -0
  166. package/payload/server/public/assets/public-CN-ltC8x.js.gz +0 -0
  167. package/payload/server/public/assets/{rotate-ccw-CcU_XD8B.js → rotate-ccw-Buopsc22.js} +1 -1
  168. package/payload/server/public/assets/rotate-ccw-Buopsc22.js.br +0 -0
  169. package/payload/server/public/assets/rotate-ccw-Buopsc22.js.gz +0 -0
  170. package/payload/server/public/assets/routines-C-yr8ddT.js +1 -0
  171. package/payload/server/public/assets/routines-C-yr8ddT.js.br +0 -0
  172. package/payload/server/public/assets/routines-C-yr8ddT.js.gz +0 -0
  173. package/payload/server/public/assets/skills-DiiuVzDK.js +1 -0
  174. package/payload/server/public/assets/skills-DiiuVzDK.js.br +0 -0
  175. package/payload/server/public/assets/skills-DiiuVzDK.js.gz +0 -0
  176. package/payload/server/public/assets/tasks-CnHIfn2_.js +1 -0
  177. package/payload/server/public/assets/tasks-CnHIfn2_.js.br +0 -0
  178. package/payload/server/public/assets/tasks-CnHIfn2_.js.gz +0 -0
  179. package/payload/server/public/assets/{time-entry-format-IFqinQJj.js → time-entry-format-BBokCF-D.js} +1 -1
  180. package/payload/server/public/assets/time-entry-format-BBokCF-D.js.br +0 -0
  181. package/payload/server/public/assets/time-entry-format-BBokCF-D.js.gz +0 -0
  182. package/payload/server/public/assets/{triangle-alert-C5vH__sq.js → triangle-alert-C_AcpTW5.js} +1 -1
  183. package/payload/server/public/assets/triangle-alert-C_AcpTW5.js.br +0 -0
  184. package/payload/server/public/assets/triangle-alert-C_AcpTW5.js.gz +0 -0
  185. package/payload/server/public/assets/{useCopyFeedback-BeL2gPl6.js → useCopyFeedback-TdimmPVR.js} +1 -1
  186. package/payload/server/public/assets/useCopyFeedback-TdimmPVR.js.br +0 -0
  187. package/payload/server/public/assets/useCopyFeedback-TdimmPVR.js.gz +0 -0
  188. package/payload/server/public/assets/useSubAccountSwitcher-BwHqV55p.js +14 -0
  189. package/payload/server/public/assets/useSubAccountSwitcher-BwHqV55p.js.br +0 -0
  190. package/payload/server/public/assets/useSubAccountSwitcher-BwHqV55p.js.gz +0 -0
  191. package/payload/server/public/assets/useSubAccountSwitcher-CsrPYqtm.css +1 -0
  192. package/payload/server/public/assets/useSubAccountSwitcher-CsrPYqtm.css.br +0 -0
  193. package/payload/server/public/assets/useSubAccountSwitcher-CsrPYqtm.css.gz +0 -0
  194. package/payload/server/public/assets/{useVoiceRecorder-Cpw3aOSt.js → useVoiceRecorder-BT2738ZS.js} +1 -1
  195. package/payload/server/public/assets/useVoiceRecorder-BT2738ZS.js.br +0 -0
  196. package/payload/server/public/assets/useVoiceRecorder-BT2738ZS.js.gz +0 -0
  197. package/payload/server/public/assets/{wrench-BBEup3hd.js → wrench-B51s5kGA.js} +1 -1
  198. package/payload/server/public/assets/wrench-B51s5kGA.js.br +0 -0
  199. package/payload/server/public/assets/wrench-B51s5kGA.js.gz +0 -0
  200. package/payload/server/public/browser.html +4 -4
  201. package/payload/server/public/calendar.html +7 -7
  202. package/payload/server/public/chat.html +13 -13
  203. package/payload/server/public/data.html +11 -11
  204. package/payload/server/public/graph.html +9 -9
  205. package/payload/server/public/index.html +14 -14
  206. package/payload/server/public/operator.html +14 -14
  207. package/payload/server/public/public.html +13 -13
  208. package/payload/server/public/routines.html +6 -6
  209. package/payload/server/public/skills.html +5 -5
  210. package/payload/server/public/tasks.html +6 -6
  211. package/payload/server/server.js +1238 -653
  212. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
  213. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
  214. package/payload/server/public/assets/AdminShell-Djg6ndne.js +0 -2
  215. package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
  216. package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
  217. package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
  218. package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
  219. package/payload/server/public/assets/admin-D9Bt47GY.js +0 -1
  220. package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
  221. package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
  222. package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
  223. package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
  224. package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
  225. package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
  226. package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
  227. package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
  228. package/payload/server/public/assets/chat-DPhT6urD.js +0 -1
  229. package/payload/server/public/assets/chat-DPhT6urD.js.br +0 -1
  230. package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
  231. package/payload/server/public/assets/chevron-left-BSdXvJcH.js +0 -1
  232. package/payload/server/public/assets/chevron-right-DI7vI3IB.js +0 -1
  233. package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
  234. package/payload/server/public/assets/clock-YUTPXZH5.js +0 -1
  235. package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
  236. package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
  237. package/payload/server/public/assets/data-CfjttTvR.js +0 -1
  238. package/payload/server/public/assets/data-CfjttTvR.js.br +0 -2
  239. package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
  240. package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
  241. package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
  242. package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
  243. package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
  244. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
  245. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
  246. package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
  247. package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
  248. package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
  249. package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
  250. package/payload/server/public/assets/page--78CbdFs.js +0 -32
  251. package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
  252. package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
  253. package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
  254. package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
  255. package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
  256. package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
  257. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
  258. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
  259. package/payload/server/public/assets/routines-CA7D0myr.js +0 -1
  260. package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
  261. package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
  262. package/payload/server/public/assets/skills-CfVPPSxO.js +0 -1
  263. package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
  264. package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
  265. package/payload/server/public/assets/tasks-DgieyZqz.js +0 -1
  266. package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
  267. package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
  268. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
  269. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
  270. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
  271. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
  272. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
  273. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
  274. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +0 -14
  275. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
  276. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
  277. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +0 -1
  278. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
  279. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
  280. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
  281. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
  282. package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
  283. package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
package/dist/index.js CHANGED
@@ -5398,16 +5398,30 @@ try {
5398
5398
  console.log("================================================================");
5399
5399
  console.log("");
5400
5400
  if (isLinux()) {
5401
+ const edgeDecision = pickEdgeBindDecision(networkInterfaces());
5401
5402
  // Only true when the edge binds 0.0.0.0. On a host classified to loopback
5402
5403
  // (Task 2082) nothing answers on the LAN address, so printing this line
5403
5404
  // there would be a false instruction; the SSH-forward recipe below is the
5404
5405
  // whole story for that host class.
5405
- if (pickEdgeBindDecision(networkInterfaces()).hostname === "0.0.0.0") {
5406
+ if (edgeDecision.hostname === "0.0.0.0") {
5406
5407
  console.log(` Same network (Pi / LAN): http://${DEVICE_HOSTNAME}.local:${PORT}`);
5407
5408
  console.log("");
5408
5409
  }
5409
- console.log(` Remote access (Hetzner / cloud) on your local machine run:`);
5410
- console.log(` ssh -L ${PORT}:localhost:${PORT} -L ${BRAND.websockifyPort}:localhost:${BRAND.websockifyPort} admin@<server-ipv4>`);
5410
+ // Provider-neutral. This recipe is identical on every cloud host with no
5411
+ // LAN to the operator, so naming one provider mis-describes the others: it
5412
+ // was printed verbatim on a netcup RS 8000 install (2026-07-28) while
5413
+ // saying "Hetzner". The host class, not the vendor, is what selects it.
5414
+ //
5415
+ // Print the host's own public address and the user that owns the install
5416
+ // rather than `admin@<server-ipv4>`. Both are already known here —
5417
+ // `publicAddresses` is what the loopback bind decision was just made from —
5418
+ // and a placeholder in a copy-pasted line is one more thing to get wrong.
5419
+ // Falls back to the placeholder only when the interface table shows no
5420
+ // public IPv4, which is the 1:1-NAT case the decision cannot see.
5421
+ const sshUser = userInfo().username;
5422
+ const sshTarget = `${sshUser}@${edgeDecision.publicAddresses[0] ?? "<server-ipv4>"}`;
5423
+ console.log(` Remote access (cloud server, no LAN to you): on your local machine run`);
5424
+ console.log(` ssh -L ${PORT}:localhost:${PORT} -L ${BRAND.websockifyPort}:localhost:${BRAND.websockifyPort} ${sshTarget}`);
5411
5425
  console.log(` Then open:`);
5412
5426
  console.log(` Dashboard: http://localhost:${PORT}`);
5413
5427
  console.log(` VNC browser (Claude OAuth + Cloudflare): http://localhost:${BRAND.websockifyPort}/vnc.html`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-sitedesk-code",
3
- "version": "0.1.520",
3
+ "version": "0.1.522",
4
4
  "description": "Install SiteDesk — automated back office for independent building contractors",
5
5
  "bin": {
6
6
  "create-sitedesk-code": "./dist/index.js"
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@maxy/account-schema-regions",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@maxy/account-schema-regions",
9
+ "version": "0.1.0"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@maxy/active-rules",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@maxy/active-rules",
9
+ "version": "0.1.0"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@maxy/mcp-lifeline",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@maxy/mcp-lifeline",
9
+ "version": "0.1.0"
10
+ }
11
+ }
12
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:69ee9681cbe3bd78fa8d38f916aa5979d4814681b59f3c1bd3570c613315ca7c
4
+ content-hash: sha256:3e9150568da700b668d3284d2edf7c780e6bc13323f5851c88371f559407b120
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -1294,6 +1294,8 @@ In netcup's Customer Control Panel:
1294
1294
  2. When the server is provisioned, install **Ubuntu 24.04 LTS** from the CCP's image list.
1295
1295
  3. Add your SSH public key during image setup, or paste it into `/root/.ssh/authorized_keys` from the remote console afterwards.
1296
1296
 
1297
+ The image form's **Timezone** dropdown offers only netcup's own datacentre zones (`Europe/Berlin`, `Europe/Vienna`), so an install serving any other region cannot set its real timezone here. Leave the default and correct it in step 2 with `timedatectl set-timezone <zone>`. This is not cosmetic: the scheduler and cron fire on system local time, so a UK install left on Berlin runs an hour early. **Locale** is a free choice on the same form and should be set there; the fleet uses `en_US.UTF-8`.
1298
+
1297
1299
  netcup does not provide a network firewall product in front of the server, so at this point the host firewall is the only inbound control. Close everything except SSH before going further:
1298
1300
 
1299
1301
  ```bash
@@ -1353,7 +1355,7 @@ Take another snapshot immediately before any subsequent platform upgrade.
1353
1355
 
1354
1356
  ## 4. Run the installer
1355
1357
 
1356
- Run as the `admin` user (do not use `sudo`; the installer escalates internally where it needs to). The `--hostname` flag is required on a cloud VM it becomes the Cloudflare-fronted hostname and the systemd unit name.
1358
+ Run as the `admin` user (do not use `sudo`; the installer escalates internally where it needs to). The `--hostname` flag is required on a cloud VM. It sets the **OS** hostname via `hostnamectl` and names the Cloudflare-fronted host; it does **not** name the systemd units, which stay brand-derived. Measured on a netcup install 2026-07-28: passing `--hostname orchard` to `create-realagent-code` produced `hostname` = `orchard` alongside units still named `realagent-code.service`, `realagent-code-edge.service`, `realagent-code-claude-session-manager.service` and `realagent-code-rss-sampler.service`. Pass the same value on every re-run, so the flag rather than OS detection decides it.
1357
1359
 
1358
1360
  ```bash
1359
1361
  npx -y @rubytech/create-maxy-code@latest --hostname <hostname>
@@ -2386,7 +2388,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2386
2388
 
2387
2389
  1. **No app to register.** The client ID ships with the brand (`brand.json#outlookClientId`), and the installer stamps it onto the session-manager env, so there is zero operator Azure work. On a brand that ships an empty value the plugin stays inert until its vendor app exists — see `platform/plugins/outlook/references/auth.md` for the one-time-per-brand vendor provisioning runbook (a vendor step, not an operator or end-user step), including setting **Allow public client flows** to Yes.
2388
2390
  2. **Per account: register the Outlook account** — in admin chat, ask the agent to "register my Outlook account". The agent runs `outlook-account-register`, which returns a short user code and the URL `microsoft.com/devicelogin` and relays both to you.
2389
- 3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds. Nothing else to do: the server finishes the sign-in automatically once you consent, usually within about half a minute, and the mailbox is then connected.
2391
+ 3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Mail.ReadWrite.Shared`, `Mail.Send.Shared`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending, `Calendars.ReadWrite` enables calendar control, and the two `.Shared` scopes let this mailbox reach a shared mailbox it has Full Access to; a mailbox last consented on an older scope set must re-register before send, any calendar write, or any shared mailbox succeeds. Nothing else to do: the server finishes the sign-in automatically once you consent, usually within about half a minute, and the mailbox is then connected.
2390
2392
  4. **Done.** Subsequent tool calls (mail, calendar, contacts) use the persisted refresh token transparently.
2391
2393
  5. **Several mailboxes on one account.** Register again to add a second mailbox; it is stored alongside the first, not in place of it. Every operational tool then takes an optional `mailbox` argument — an email address or graphUserId. With one mailbox, omit it. With two or more, name it; omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows every connected mailbox. Only one registration runs at a time per account; starting another while one is live returns an in-progress signal.
2392
2394
 
@@ -2413,12 +2415,32 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2413
2415
  | `outlook-calendar-freebusy` | Availability for one or more addresses over a window. |
2414
2416
  | `outlook-contacts-list` | Top contacts. Default top=50. |
2415
2417
  | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
2416
- | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
2418
+ | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health, signed-in and shared alike (`kind`, and `via` for a shared one). Local only by default — no network — so it also flags a mailbox whose stored credentials cannot be read. `verify:true` additionally probes each shared mailbox and reports `reachable`. |
2419
+ | `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
2420
+ | `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
2417
2421
  | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
2418
2422
  | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
2419
2423
  | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
2420
2424
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
2421
2425
 
2426
+ ## Shared mailboxes
2427
+
2428
+ A shared mailbox (`team@`, `info@`, `accounts@`) has no sign-in of its own, so nobody can connect it the way a normal mailbox is connected. **It does not need a licence and should never be given one for this.** Whoever has Full Access to it connects their own mailbox in the normal way, then attaches the shared one to it:
2429
+
2430
+ ```
2431
+ outlook-shared-mailbox-add address=team@yourcompany.com
2432
+ ```
2433
+
2434
+ Add `via=<your address>` if this account has more than one mailbox connected. From then on the shared address works as the `mailbox` argument on any mail tool, and `outlook-mailbox-list` shows it as `kind:delegated`.
2435
+
2436
+ The attach call checks the access before it stores anything, so what it says back is the real state of things:
2437
+
2438
+ - **"does not have Full Access"** — the delegation is missing. Grant it in the Microsoft 365 admin centre: Teams & groups → Shared mailboxes → the mailbox → Members / Read and manage. Nothing is purchased.
2439
+ - **"no mailbox with this address in the tenant"** — the address is wrong, or it is a distribution list. A distribution list has no mailbox and cannot be attached.
2440
+ - **"connected without the Mail.ReadWrite.Shared scope"** — your mailbox was connected before the plugin asked for the shared scopes. Register it again and it will work.
2441
+
2442
+ Mail only: calendar and contacts tools refuse on a shared mailbox and say so. If a shared mailbox stops answering, the delegation was probably removed in Exchange — `outlook-mailbox-list verify:true` reports which ones are still reachable.
2443
+
2422
2444
  ## Drafting to a Contact
2423
2445
 
2424
2446
  When the admin agent drafts or sends a message to a known contact, it reads that contact's record first and applies any routing the record carries, for example a preferred send-to address, showing the result for your approval before anything is sent.
@@ -2438,7 +2460,10 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2438
2460
  | /me failed post-token | `me-failed account=<id> status=<N>` |
2439
2461
  | Auth ok | `auth-ok account=<id> graphUserId=<id> mail=<addr> scopes=<csv> tokenExpSec=<N>` |
2440
2462
  | Registration already in flight | `register-in-progress account=<id>` (start refused; one device-code flow at a time) |
2441
- | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal |
2463
+ | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused kind=signed-in|delegated via=<delegator-or-empty>` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal, and `kind=delegated` means the call ran against a shared mailbox through `via` |
2464
+ | Shared mailbox attached | `shared-add account=<id> address=<addr> via=<key> outcome=ok|ok-reattached|no-scope|forbidden|not-found` — every refusal path is named, so a failed attach is diagnosable without reproducing it |
2465
+ | Shared mailbox detached | `shared-remove account=<id> address=<addr> outcome=ok|absent` |
2466
+ | Calendar/contacts on a shared mailbox | `shared-tool-refused account=<id> tool=<tool> prefix=<users-path>` — refused before any request is built |
2442
2467
  | Token refreshed | `token-refreshed account=<id> oldExpSec=<N> newExpSec=<N>` |
2443
2468
  | Refresh failed | `token-refresh-failed account=<id> reason=<err>` (terminal) |
2444
2469
  | Mail list | `mail-list op=resolve account=<id> folderIn=<name> folderResolved=<token> asRawId=<bool> count=<N> elapsedMs=<N>` (on error, `error=<msg>` replaces `count`) |
@@ -2456,7 +2481,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2456
2481
  | Scope insufficient | `scope-insufficient tool=<t> account=<id> missing=Calendars.ReadWrite` (stale read-only consent) |
2457
2482
  | Contacts list | `contacts-list account=<id> count=<N> elapsedMs=<N>` |
2458
2483
  | Mailbox info | `mailbox-info account=<id> tokenWithinRefreshWindow=<bool> folderCount=<N>` |
2459
- | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N>` (unreadable>0 flags an orphaned or corrupt mailbox store) |
2484
+ | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N> addressable=<N> delegated=<N> verified=<N>` — `count` covers signed-in plus delegated entries, `addressable` counts only signed-in ones that are usable by their email, `delegated` counts the attached shared mailboxes, and `verified` is how many of those were probed (0 unless `verify:true`). `unreadable>0` flags an orphaned or corrupt mailbox store |
2460
2485
  | Graph error | `graph-error account=<id> status=<N> code=<graphErrorCode> retryAfterMs=<N-or-null>` — writes flow through the same `callGraph` loop, so a failed write logs `graph-error` too |
2461
2486
  | On-prem rejected | `on-prem-rejected account=<id> mailServer=<host>` (terminal) |
2462
2487
  | Scope insufficient | `scope-insufficient account=<id> tool=<t> missing=Mail.Send` (terminal — re-register to grant send) |
@@ -3246,7 +3271,7 @@ either is a regression.
3246
3271
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
3247
3272
  | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss. On a `whatsapp` row the number is printed in international form (`+44 7504 472444`, `+33 7 68 24 30 67`) by `formatSenderPhone` — `normalizeE164` then libphonenumber-js `formatInternational`, gated on `isPossible` so an unresolved LID prints raw; a `telegram` senderId is a Telegram user id and a `webchat` one is `session:<id>`, so neither is regrouped. The formatting is display-only: row identity, matching, and store keys stay on the normalized digits. Because the label function is shared, a Sessions row and that conversation's reader-panel row read identically. A channel row name has two sources, in precedence order. First the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, which matches the sender phone against the account's admins — that names an admin WhatsApp session and every Telegram session. Second, for a `whatsapp` row only, the WhatsApp pushName from the account's on-disk message store (`storePushNamesBySender` over `readConversationSummaries`, keyed by `normalizeE164` of the counterparty phone so a `+44 7…` sender matches a `447…` store key). The store is consulted only when the resolver reports **no binding** for that sender. That is typically a public WhatsApp customer, the case this exists for — such a sender has no admin record and so can only be named by the store, and without it those rows degraded to the bare number while the reader panel named them — though an admin missing from `users.json` qualifies too. A resolver miss caused by Neo4j being unreachable deliberately does NOT qualify: that is an outage rather than an absent binding, so the row degrades to the number and still logs `reason=neo4j-down`, keeping the outage signal intact. Telegram has no message store and is never looked up. The store read is guarded: a corrupt or absent store degrades to no pushName and logs `op=store-unreadable`, never 500s the list. Both sources are the ones the reader itself uses, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The Sessions list reads the viewed account's own store only. An account-manager's thread persists under the house account rather than the sub-account it manages, so on a multi-account install viewing a sub-account that row is not named here even though the reader panel names it — the reader additionally consults the house store, filtered to manager phones bound to that sub. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> (operator=<n> store=<n>) unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line — emitted only when **both** sources miss, so a residual shortfall means a genuinely nameless sender rather than the expected public-customer floor, and the `operator=`/`store=` split says which source is carrying the names. | `GET /` |
3248
3273
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
3249
- | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
3274
+ | `/session-rc-spawn` | POST `{ sessionId?, name?, title?, model?, effort? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. **Naming.** `title` is the operator's name for a NEW session, typed in the naming modal that gates all four New-session controls (sidebar CTA, composer popover, zero-sessions splash, Conversations flyout); nothing is POSTed until the operator confirms it, and the modal prefills `New session: <date, time>`. `resolveSpawnPlan` trims it, it never rides the manager spawn payload, and this route writes it to the user-title store through the manager's `POST /:id/rename` **before it answers**, so the client cannot navigate ahead of the write; the response gains `titleSource: 'user' \| 'none'`. A resume carries no title. A rejected or unreachable rename leaves the spawn intact and returns `'none'`. Lifeline: `[chat-spawn] op=title spawnReqId=… sessionId=… titleLen=… source=modal\|absent outcome=applied\|rejected\|failed`, where `source=absent` on a new spawn is the standing signal that a control bypassed the modal. | `POST /` |
3250
3275
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
3251
3276
 
3252
3277
  **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. **For admin sessions, permission mode is fixed (Bypass) and not operator-chosen, while model and effort ARE operator-chosen at launch** via the composer's `+` new-session popover — a radio model list (Opus / Sonnet / Haiku) plus a 5-stop Faster→Smarter effort slider (Low / Medium / High / Extra / Max, where `max` applies via the `--effort` argv rather than `settings.json`); the popover description lives in `.docs/admin-webchat-native-channel.md`. Absent a pick (and on every resume), the daemon-pinned defaults govern: the `/chat` composer *footer* carries no live re-seat pickers — `rc-daemon.ts` pins settings.json `model`/`effortLevel`/`permissions.defaultMode` to `claude-opus-5[1m]` / `medium` / `bypassPermissions` regardless of `account.json` (read by the rc-spawn webchat path), and `pty-spawner.ts` forces the claude.ai/code PTY `--permission-mode` to `bypassPermissions` for `role=admin`. Public sessions are fixed to `claude-sonnet-5` (the `/public-spawn` chokepoint + `buildPublicWebchatSpawnRequest`), keeping `dontAsk`. **WhatsApp admin sessions override the daemon default:** the `/rc-spawn` handler pins `claude-sonnet-5` / `--effort max` / `bypassPermissions` for a `role=admin` WhatsApp spawn (constants `WHATSAPP_ADMIN_MODEL`/`WHATSAPP_ADMIN_EFFORT` in `http-server.ts`), pushing `--model`/`--effort` on both the fresh spawn and every resume so the params hold for the session's whole life. The override is scoped to sessions *born* under it: a fresh WhatsApp admin spawn stamps its classification sidecar `model=claude-sonnet-5`, and a resume re-asserts Sonnet/max only when that stamp is present — so a WhatsApp session created before this change (sidecar `model=null`) finishes its life on the Opus/medium default. Webchat and Telegram admin sessions are unaffected; public WhatsApp inbound never spawns. The composer footer instead renders a **read-only** params row (mode · model · effort labels) plus a context-window usage figure (`pct% · ~used/~window`), all sourced from the session pointer; the full path is in `.docs/admin-webchat-native-channel.md`. The composer cannot re-seat a session, so the "no transcript" 400 on a brand-new chat is unreachable; the `/api/admin/session-reseat` + `/api/webchat/settings` routes and the dashboard re-seat form stay as off-composer surfaces. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
@@ -3900,6 +3925,16 @@ authoritative. This section names the surfaces and what backs each.
3900
3925
  > - **Dashboard home** renders on `/` (a greeting, stat cards, a Live-now column,
3901
3926
  > a Recent artefacts card, and a Channels column), consuming existing endpoints
3902
3927
  > only.
3928
+ > - **Channels card rows.** A WhatsApp row shows a profile picture,
3929
+ > the conversation's name, its last message, and the time, matching the layout
3930
+ > of the WhatsApp client. The name is the one WhatsApp itself holds: a group
3931
+ > shows its group subject, a 1:1 shows the contact name saved on the paired
3932
+ > phone. A group row carries an extra `Group` chip. When no name has been
3933
+ > captured yet, a group falls back to its group id and a 1:1 to its phone
3934
+ > number; neither ever shows a group member's name or the install's own
3935
+ > WhatsApp display name. The existing `Public` chip (the message came from
3936
+ > outside) and `House` / `Client` chip (which account owns the thread) are
3937
+ > unchanged.
3903
3938
  > - **Per-surface header.** Each surface carries its own header (`PageHeader`):
3904
3939
  > the sidebar collapse toggle, the page title, and a Claude status pill on the
3905
3940
  > right (Connected / Connect, driven by `/api/health`). The pill replaces the
@@ -5030,8 +5065,12 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
5030
5065
 
5031
5066
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
5032
5067
 
5068
+ A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one, which is how a resume the session manager redirected to a live fork re-points itself rather than staying on the dead source. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
5069
+
5033
5070
  Operator audit cyphers:
5034
5071
  - "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
5072
+ - "Which session is this task worked in?" — `MATCH (t:Task {taskId:$id})-[:WORKED_ON_IN]->(c:Conversation) RETURN c.sessionId, c.name`
5073
+ - "Which tasks were marked for a session spawn that never linked?" — `MATCH (t:Task {accountId:$acct}) WHERE t.sessionSpawnRequestedAt IS NOT NULL AND NOT (t)-[:WORKED_ON_IN]->(:Conversation) RETURN t.taskId, t.name, t.sessionSpawnRequestedAt`
5035
5074
  - "What cloudflare resources did this tunnel-login produce?" — `MATCH (t:Task {kind:'cloudflare-tunnel-login', status:'completed'})-[:PRODUCED]->(r) RETURN t.taskId, r.tunnelId, r.hostnameValue ORDER BY t.completedAt DESC`
5036
5075
 
5037
5076
  See `.docs/neo4j.md § Process provenance doctrine` for the full enforcement contract, observability surface, and out-of-scope deferrals.
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-07-28 (0.1.522)
13
+
14
+ - You can now connect a shared Outlook mailbox you have been given access to, without needing a separate licence for it.
15
+ - WhatsApp conversations now show the real chat name, contact avatar and last message, instead of guessing from the first message.
16
+ - Open tasks now have a Session button that opens or resumes a chat session named for that task.
17
+
18
+ ## 2026-07-28 (0.1.521)
19
+
20
+ - Starting a new session now opens a naming modal first, and New-session controls read white on black across every brand.
21
+ - WhatsApp conversation reader now identifies group message senders by their real JID, and a turn no longer reports a still-in-flight attachment as missing.
22
+ - A stale pending-interactive entry no longer sends its narration to the customer; the calendar attachment diagnostic now names the flat single-part shape it used to drop.
23
+
12
24
  ## 2026-07-28 (0.1.520)
13
25
 
14
26
  - The admin now lands on the Dashboard, with a Public pages card linking the booking site and file-drop portal.
@@ -58,7 +58,7 @@ either is a regression.
58
58
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
59
59
  | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss. On a `whatsapp` row the number is printed in international form (`+44 7504 472444`, `+33 7 68 24 30 67`) by `formatSenderPhone` — `normalizeE164` then libphonenumber-js `formatInternational`, gated on `isPossible` so an unresolved LID prints raw; a `telegram` senderId is a Telegram user id and a `webchat` one is `session:<id>`, so neither is regrouped. The formatting is display-only: row identity, matching, and store keys stay on the normalized digits. Because the label function is shared, a Sessions row and that conversation's reader-panel row read identically. A channel row name has two sources, in precedence order. First the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, which matches the sender phone against the account's admins — that names an admin WhatsApp session and every Telegram session. Second, for a `whatsapp` row only, the WhatsApp pushName from the account's on-disk message store (`storePushNamesBySender` over `readConversationSummaries`, keyed by `normalizeE164` of the counterparty phone so a `+44 7…` sender matches a `447…` store key). The store is consulted only when the resolver reports **no binding** for that sender. That is typically a public WhatsApp customer, the case this exists for — such a sender has no admin record and so can only be named by the store, and without it those rows degraded to the bare number while the reader panel named them — though an admin missing from `users.json` qualifies too. A resolver miss caused by Neo4j being unreachable deliberately does NOT qualify: that is an outage rather than an absent binding, so the row degrades to the number and still logs `reason=neo4j-down`, keeping the outage signal intact. Telegram has no message store and is never looked up. The store read is guarded: a corrupt or absent store degrades to no pushName and logs `op=store-unreadable`, never 500s the list. Both sources are the ones the reader itself uses, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The Sessions list reads the viewed account's own store only. An account-manager's thread persists under the house account rather than the sub-account it manages, so on a multi-account install viewing a sub-account that row is not named here even though the reader panel names it — the reader additionally consults the house store, filtered to manager phones bound to that sub. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> (operator=<n> store=<n>) unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line — emitted only when **both** sources miss, so a residual shortfall means a genuinely nameless sender rather than the expected public-customer floor, and the `operator=`/`store=` split says which source is carrying the names. | `GET /` |
60
60
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
61
- | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
61
+ | `/session-rc-spawn` | POST `{ sessionId?, name?, title?, model?, effort? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. **Naming.** `title` is the operator's name for a NEW session, typed in the naming modal that gates all four New-session controls (sidebar CTA, composer popover, zero-sessions splash, Conversations flyout); nothing is POSTed until the operator confirms it, and the modal prefills `New session: <date, time>`. `resolveSpawnPlan` trims it, it never rides the manager spawn payload, and this route writes it to the user-title store through the manager's `POST /:id/rename` **before it answers**, so the client cannot navigate ahead of the write; the response gains `titleSource: 'user' \| 'none'`. A resume carries no title. A rejected or unreachable rename leaves the spawn intact and returns `'none'`. Lifeline: `[chat-spawn] op=title spawnReqId=… sessionId=… titleLen=… source=modal\|absent outcome=applied\|rejected\|failed`, where `source=absent` on a new spawn is the standing signal that a control bypassed the modal. | `POST /` |
62
62
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
63
63
 
64
64
  **Admin session management moved entirely to claude's own interfaces** (claude.ai/code, claude desktop). A manager-owned per-account `claude rc --spawn same-dir` daemon registers the device as a Remote Control target there; the composer creates / resumes / stops / renames / archives / deletes sessions, with model + permission-mode applied at inception. The model lever is `account.json.adminModel` → `CLAUDE_CONFIG_DIR/settings.json "model"`, written by the daemon supervisor at boot; the reasoning-effort lever is `account.json.effort` → `settings.json "effortLevel"` (accepted values `low|medium|high|xhigh`; any other value, e.g. legacy `"auto"`, leaves the key unset so claude's own default governs), written by the same read-merge-write at boot. A third inception lever, permission mode, is added: `account.json.adminPermissionMode` → `settings.json "permissions.defaultMode"` (the 5 composer-writable modes `default|acceptEdits|plan|auto|bypassPermissions`; `dontAsk` is out of scope). Unlike model/effort (top-level keys), this lever **deep-merges** the nested `permissions.defaultMode`, owning only that key and preserving sibling `permissions` keys (allow/deny/…) — what the binary and the spawn lifeline actually read. **For admin sessions, permission mode is fixed (Bypass) and not operator-chosen, while model and effort ARE operator-chosen at launch** via the composer's `+` new-session popover — a radio model list (Opus / Sonnet / Haiku) plus a 5-stop Faster→Smarter effort slider (Low / Medium / High / Extra / Max, where `max` applies via the `--effort` argv rather than `settings.json`); the popover description lives in `.docs/admin-webchat-native-channel.md`. Absent a pick (and on every resume), the daemon-pinned defaults govern: the `/chat` composer *footer* carries no live re-seat pickers — `rc-daemon.ts` pins settings.json `model`/`effortLevel`/`permissions.defaultMode` to `claude-opus-5[1m]` / `medium` / `bypassPermissions` regardless of `account.json` (read by the rc-spawn webchat path), and `pty-spawner.ts` forces the claude.ai/code PTY `--permission-mode` to `bypassPermissions` for `role=admin`. Public sessions are fixed to `claude-sonnet-5` (the `/public-spawn` chokepoint + `buildPublicWebchatSpawnRequest`), keeping `dontAsk`. **WhatsApp admin sessions override the daemon default:** the `/rc-spawn` handler pins `claude-sonnet-5` / `--effort max` / `bypassPermissions` for a `role=admin` WhatsApp spawn (constants `WHATSAPP_ADMIN_MODEL`/`WHATSAPP_ADMIN_EFFORT` in `http-server.ts`), pushing `--model`/`--effort` on both the fresh spawn and every resume so the params hold for the session's whole life. The override is scoped to sessions *born* under it: a fresh WhatsApp admin spawn stamps its classification sidecar `model=claude-sonnet-5`, and a resume re-asserts Sonnet/max only when that stamp is present — so a WhatsApp session created before this change (sidecar `model=null`) finishes its life on the Opus/medium default. Webchat and Telegram admin sessions are unaffected; public WhatsApp inbound never spawns. The composer footer instead renders a **read-only** params row (mode · model · effort labels) plus a context-window usage figure (`pct% · ~used/~window`), all sourced from the session pointer; the full path is in `.docs/admin-webchat-native-channel.md`. The composer cannot re-seat a session, so the "no transcript" 400 on a brand-new chat is unreachable; the `/api/admin/session-reseat` + `/api/webchat/settings` routes and the dashboard re-seat form stay as off-composer surfaces. When the live session runs in `default` ("Ask permissions") mode, a tool the agent attempts that needs approval surfaces an Allow/Deny card in the composer (tool, description, argument preview); a click sends the verdict over Claude Code's channel permission relay and the tool runs or is refused — webchat is the only interactive ask surface (WhatsApp stays text-only; Telegram has no agent channel). Beyond the composer, the maxy admin UI keeps a single "New session" link (`https://claude.ai/code`, opens in a new tab) and no separate session list, viewer, controls, or model/mode picker. The daemon supervisor lives at [`platform/services/claude-session-manager/src/rc-daemon.ts`](../../../services/claude-session-manager/src/rc-daemon.ts). The `/session-defaults` route and `SpawnPreference` node were deleted with the picker. `/new-session-failure`, `/new-session-submit`, and `/claude-capabilities` are now orphaned (consumed only by the deleted NewSessionModal) — see [`.tasks/501`](../../../.tasks/) for their removal.
@@ -712,6 +712,16 @@ authoritative. This section names the surfaces and what backs each.
712
712
  > - **Dashboard home** renders on `/` (a greeting, stat cards, a Live-now column,
713
713
  > a Recent artefacts card, and a Channels column), consuming existing endpoints
714
714
  > only.
715
+ > - **Channels card rows.** A WhatsApp row shows a profile picture,
716
+ > the conversation's name, its last message, and the time, matching the layout
717
+ > of the WhatsApp client. The name is the one WhatsApp itself holds: a group
718
+ > shows its group subject, a 1:1 shows the contact name saved on the paired
719
+ > phone. A group row carries an extra `Group` chip. When no name has been
720
+ > captured yet, a group falls back to its group id and a 1:1 to its phone
721
+ > number; neither ever shows a group member's name or the install's own
722
+ > WhatsApp display name. The existing `Public` chip (the message came from
723
+ > outside) and `House` / `Client` chip (which account owns the thread) are
724
+ > unchanged.
715
725
  > - **Per-surface header.** Each surface carries its own header (`PageHeader`):
716
726
  > the sidebar collapse toggle, the page title, and a Claude status pill on the
717
727
  > right (Connected / Connect, driven by `/api/health`). The pill replaces the
@@ -560,8 +560,12 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
560
560
 
561
561
  Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
562
562
 
563
+ A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one, which is how a resume the session manager redirected to a live fork re-points itself rather than staying on the dead source. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
564
+
563
565
  Operator audit cyphers:
564
566
  - "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
567
+ - "Which session is this task worked in?" — `MATCH (t:Task {taskId:$id})-[:WORKED_ON_IN]->(c:Conversation) RETURN c.sessionId, c.name`
568
+ - "Which tasks were marked for a session spawn that never linked?" — `MATCH (t:Task {accountId:$acct}) WHERE t.sessionSpawnRequestedAt IS NOT NULL AND NOT (t)-[:WORKED_ON_IN]->(:Conversation) RETURN t.taskId, t.name, t.sessionSpawnRequestedAt`
565
569
  - "What cloudflare resources did this tunnel-login produce?" — `MATCH (t:Task {kind:'cloudflare-tunnel-login', status:'completed'})-[:PRODUCED]->(r) RETURN t.taskId, r.tunnelId, r.hostnameValue ORDER BY t.completedAt DESC`
566
570
 
567
571
  See `.docs/neo4j.md § Process provenance doctrine` for the full enforcement contract, observability surface, and out-of-scope deferrals.
@@ -6,7 +6,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
6
6
 
7
7
  1. **No app to register.** The client ID ships with the brand (`brand.json#outlookClientId`), and the installer stamps it onto the session-manager env, so there is zero operator Azure work. On a brand that ships an empty value the plugin stays inert until its vendor app exists — see `platform/plugins/outlook/references/auth.md` for the one-time-per-brand vendor provisioning runbook (a vendor step, not an operator or end-user step), including setting **Allow public client flows** to Yes.
8
8
  2. **Per account: register the Outlook account** — in admin chat, ask the agent to "register my Outlook account". The agent runs `outlook-account-register`, which returns a short user code and the URL `microsoft.com/devicelogin` and relays both to you.
9
- 3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds. Nothing else to do: the server finishes the sign-in automatically once you consent, usually within about half a minute, and the mailbox is then connected.
9
+ 3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Mail.ReadWrite.Shared`, `Mail.Send.Shared`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending, `Calendars.ReadWrite` enables calendar control, and the two `.Shared` scopes let this mailbox reach a shared mailbox it has Full Access to; a mailbox last consented on an older scope set must re-register before send, any calendar write, or any shared mailbox succeeds. Nothing else to do: the server finishes the sign-in automatically once you consent, usually within about half a minute, and the mailbox is then connected.
10
10
  4. **Done.** Subsequent tool calls (mail, calendar, contacts) use the persisted refresh token transparently.
11
11
  5. **Several mailboxes on one account.** Register again to add a second mailbox; it is stored alongside the first, not in place of it. Every operational tool then takes an optional `mailbox` argument — an email address or graphUserId. With one mailbox, omit it. With two or more, name it; omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows every connected mailbox. Only one registration runs at a time per account; starting another while one is live returns an in-progress signal.
12
12
 
@@ -33,12 +33,32 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
33
33
  | `outlook-calendar-freebusy` | Availability for one or more addresses over a window. |
34
34
  | `outlook-contacts-list` | Top contacts. Default top=50. |
35
35
  | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
36
- | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
36
+ | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health, signed-in and shared alike (`kind`, and `via` for a shared one). Local only by default — no network — so it also flags a mailbox whose stored credentials cannot be read. `verify:true` additionally probes each shared mailbox and reports `reachable`. |
37
+ | `outlook-shared-mailbox-add` | Attach a shared mailbox (no sign-in, no licence) through the Full Access of a mailbox that is already connected. `address` is the shared mailbox; `via` names the connected mailbox when more than one is attached. Proves the access before storing anything, so a refusal is the real answer. |
38
+ | `outlook-shared-mailbox-remove` | Detach a shared mailbox. Drops the pointer only; no credential is touched. |
37
39
  | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
38
40
  | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
39
41
  | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
40
42
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
41
43
 
44
+ ## Shared mailboxes
45
+
46
+ A shared mailbox (`team@`, `info@`, `accounts@`) has no sign-in of its own, so nobody can connect it the way a normal mailbox is connected. **It does not need a licence and should never be given one for this.** Whoever has Full Access to it connects their own mailbox in the normal way, then attaches the shared one to it:
47
+
48
+ ```
49
+ outlook-shared-mailbox-add address=team@yourcompany.com
50
+ ```
51
+
52
+ Add `via=<your address>` if this account has more than one mailbox connected. From then on the shared address works as the `mailbox` argument on any mail tool, and `outlook-mailbox-list` shows it as `kind:delegated`.
53
+
54
+ The attach call checks the access before it stores anything, so what it says back is the real state of things:
55
+
56
+ - **"does not have Full Access"** — the delegation is missing. Grant it in the Microsoft 365 admin centre: Teams & groups → Shared mailboxes → the mailbox → Members / Read and manage. Nothing is purchased.
57
+ - **"no mailbox with this address in the tenant"** — the address is wrong, or it is a distribution list. A distribution list has no mailbox and cannot be attached.
58
+ - **"connected without the Mail.ReadWrite.Shared scope"** — your mailbox was connected before the plugin asked for the shared scopes. Register it again and it will work.
59
+
60
+ Mail only: calendar and contacts tools refuse on a shared mailbox and say so. If a shared mailbox stops answering, the delegation was probably removed in Exchange — `outlook-mailbox-list verify:true` reports which ones are still reachable.
61
+
42
62
  ## Drafting to a Contact
43
63
 
44
64
  When the admin agent drafts or sends a message to a known contact, it reads that contact's record first and applies any routing the record carries, for example a preferred send-to address, showing the result for your approval before anything is sent.
@@ -58,7 +78,10 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
58
78
  | /me failed post-token | `me-failed account=<id> status=<N>` |
59
79
  | Auth ok | `auth-ok account=<id> graphUserId=<id> mail=<addr> scopes=<csv> tokenExpSec=<N>` |
60
80
  | Registration already in flight | `register-in-progress account=<id>` (start refused; one device-code flow at a time) |
61
- | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal |
81
+ | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused kind=signed-in|delegated via=<delegator-or-empty>` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal, and `kind=delegated` means the call ran against a shared mailbox through `via` |
82
+ | Shared mailbox attached | `shared-add account=<id> address=<addr> via=<key> outcome=ok|ok-reattached|no-scope|forbidden|not-found` — every refusal path is named, so a failed attach is diagnosable without reproducing it |
83
+ | Shared mailbox detached | `shared-remove account=<id> address=<addr> outcome=ok|absent` |
84
+ | Calendar/contacts on a shared mailbox | `shared-tool-refused account=<id> tool=<tool> prefix=<users-path>` — refused before any request is built |
62
85
  | Token refreshed | `token-refreshed account=<id> oldExpSec=<N> newExpSec=<N>` |
63
86
  | Refresh failed | `token-refresh-failed account=<id> reason=<err>` (terminal) |
64
87
  | Mail list | `mail-list op=resolve account=<id> folderIn=<name> folderResolved=<token> asRawId=<bool> count=<N> elapsedMs=<N>` (on error, `error=<msg>` replaces `count`) |
@@ -76,7 +99,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
76
99
  | Scope insufficient | `scope-insufficient tool=<t> account=<id> missing=Calendars.ReadWrite` (stale read-only consent) |
77
100
  | Contacts list | `contacts-list account=<id> count=<N> elapsedMs=<N>` |
78
101
  | Mailbox info | `mailbox-info account=<id> tokenWithinRefreshWindow=<bool> folderCount=<N>` |
79
- | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N>` (unreadable>0 flags an orphaned or corrupt mailbox store) |
102
+ | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N> addressable=<N> delegated=<N> verified=<N>` — `count` covers signed-in plus delegated entries, `addressable` counts only signed-in ones that are usable by their email, `delegated` counts the attached shared mailboxes, and `verified` is how many of those were probed (0 unless `verify:true`). `unreadable>0` flags an orphaned or corrupt mailbox store |
80
103
  | Graph error | `graph-error account=<id> status=<N> code=<graphErrorCode> retryAfterMs=<N-or-null>` — writes flow through the same `callGraph` loop, so a failed write logs `graph-error` too |
81
104
  | On-prem rejected | `on-prem-rejected account=<id> mailServer=<host>` (terminal) |
82
105
  | Scope insufficient | `scope-insufficient account=<id> tool=<t> missing=Mail.Send` (terminal — re-register to grant send) |
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { walkAttachmentMetadata, countBodyStructureLeaves } from '../lib/imap.js';
2
+ import { walkAttachmentMetadata, countBodyStructureLeaves, isFlatCalendarRoot } from '../lib/imap.js';
3
3
  // Measured on the live mailbox 2026-07-27: UIDs 5104/5105, Outlook meeting
4
4
  // invites from ebony@magneticbd.co.uk. The calendar leaf carries no
5
5
  // disposition and no name, which is why it was dropped (Task 2066).
@@ -80,6 +80,37 @@ describe('Task 2066 — text/calendar leaves are attachment-shaped', () => {
80
80
  ]);
81
81
  });
82
82
  });
83
+ describe('Task 2068 — a message whose whole content type is text/calendar', () => {
84
+ // Measured 2026-07-28 against mail.privateemail.com, mailbox info@rubytech.llc:
85
+ // an APPENDed flat invite came back with no `part` on the root node, and
86
+ // BODY[1] returned the decoded VEVENT text (301 bytes).
87
+ const FLAT_INVITE = {
88
+ type: 'text/calendar',
89
+ parameters: { charset: 'utf-8', method: 'REQUEST' },
90
+ encoding: 'base64',
91
+ size: 416,
92
+ };
93
+ it('reports the root calendar node as part 1', () => {
94
+ expect(walkAttachmentMetadata(FLAT_INVITE)).toEqual([
95
+ { partKey: '1', filename: 'invite-1.ics', mimeType: 'text/calendar', sizeBytes: 416, encoding: 'base64' },
96
+ ]);
97
+ });
98
+ it('still excludes a flat text/plain root', () => {
99
+ expect(walkAttachmentMetadata({ type: 'text/plain', encoding: '7bit', size: 120 })).toEqual([]);
100
+ });
101
+ it('does not admit a flat root of another type that carries an attachment disposition', () => {
102
+ // Out of scope for 2068 — no such message has been measured, and a guessed
103
+ // part key would fetch the wrong bytes.
104
+ const flatPdf = {
105
+ type: 'application/pdf',
106
+ disposition: 'attachment',
107
+ dispositionParameters: { filename: 'report.pdf' },
108
+ encoding: 'base64',
109
+ size: 9000,
110
+ };
111
+ expect(walkAttachmentMetadata(flatPdf)).toEqual([]);
112
+ });
113
+ });
83
114
  describe('Task 2066 — leaf count for the attachments diag', () => {
84
115
  it('counts every non-multipart leaf of the invite', () => {
85
116
  expect(countBodyStructureLeaves(INVITE)).toBe(3);
@@ -91,4 +122,18 @@ describe('Task 2066 — leaf count for the attachments diag', () => {
91
122
  expect(countBodyStructureLeaves(undefined)).toBe(0);
92
123
  });
93
124
  });
125
+ describe('Task 2068 — the flat calendar shape is named in the diag', () => {
126
+ it('is true for a root calendar node with no part key', () => {
127
+ expect(isFlatCalendarRoot({ type: 'text/calendar', encoding: 'base64', size: 416 })).toBe(true);
128
+ });
129
+ it('is false for a flat text/plain root', () => {
130
+ expect(isFlatCalendarRoot({ type: 'text/plain', size: 120 })).toBe(false);
131
+ });
132
+ it('is false for a calendar part inside a multipart message', () => {
133
+ expect(isFlatCalendarRoot(INVITE)).toBe(false);
134
+ });
135
+ it('is false for an absent structure', () => {
136
+ expect(isFlatCalendarRoot(undefined)).toBe(false);
137
+ });
138
+ });
94
139
  //# sourceMappingURL=attachment-metadata-calendar.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-metadata-calendar.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-metadata-calendar.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"attachment-metadata-calendar.test.js","sourceRoot":"","sources":["../../src/__tests__/attachment-metadata-calendar.test.ts"],"names":[],"mappings":"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"}
@@ -120,6 +120,11 @@ interface BodyStructureNode {
120
120
  *
121
121
  * Inline parts without a filename (e.g. the message body) are excluded.
122
122
  *
123
+ * A message whose whole Content-Type is text/calendar has no MIME part number
124
+ * of its own — ImapFlow only numbers a node when its walk path is non-empty —
125
+ * and is reported as part "1", the RFC 3501 number for a non-multipart
126
+ * message's text (Task 2068). No other flat root is admitted this way.
127
+ *
123
128
  * A single-part message embedded in a message/rfc822 part reuses the wrapper's
124
129
  * part number rather than getting its own (ImapFlow tools.js `walk(node, path)`
125
130
  * for message/rfc822, deliberately, per RFC 3501 part numbering). Fetching that
@@ -136,6 +141,15 @@ export declare function walkAttachmentMetadata(structure: BodyStructureNode | un
136
141
  * ambiguity that hid Task 2066's dropped calendar part.
137
142
  */
138
143
  export declare function countBodyStructureLeaves(structure: BodyStructureNode | undefined): number;
144
+ /**
145
+ * True when a message's whole content type is `text/calendar` — a non-multipart
146
+ * root node, which ImapFlow leaves without a part key. Reported in the
147
+ * attachments diag so this shape stays greppable now that it is handled: with
148
+ * one leaf, Task 2066's `attachments=0 leaves>1` signal cannot distinguish it
149
+ * from a body-only message, so the next case of this class would otherwise be
150
+ * undetectable in the log (Task 2068).
151
+ */
152
+ export declare function isFlatCalendarRoot(structure: BodyStructureNode | undefined): boolean;
139
153
  export declare function formatBytes(n: number): string;
140
154
  /**
141
155
  * Format an AttachmentMeta[] as a single-line operator-readable footer.