@rubytech/create-maxy-code 0.1.382 → 0.1.384

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 (196) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/mcp/dist/index.js +37 -9
  3. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  4. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +48 -0
  5. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -1
  6. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +51 -0
  7. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -1
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -24
  9. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +7 -4
  10. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +9 -6
  11. package/payload/platform/plugins/docs/references/admin-ui.md +33 -22
  12. package/payload/platform/plugins/docs/references/attachments.md +5 -1
  13. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +1 -1
  14. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +2 -0
  15. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.d.ts +2 -0
  16. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.js +23 -0
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/d1-command.test.js.map +1 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.d.ts +12 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.d.ts.map +1 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.js +14 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/d1-command.js.map +1 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +3 -1
  24. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  25. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +2 -0
  26. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +1 -1
  27. package/payload/platform/scripts/__tests__/check-plugin-references.test.sh +83 -0
  28. package/payload/platform/scripts/check-plugin-references.mjs +60 -11
  29. package/payload/platform/services/claude-session-manager/dist/account-title-stores.d.ts +21 -0
  30. package/payload/platform/services/claude-session-manager/dist/account-title-stores.d.ts.map +1 -0
  31. package/payload/platform/services/claude-session-manager/dist/account-title-stores.js +71 -0
  32. package/payload/platform/services/claude-session-manager/dist/account-title-stores.js.map +1 -0
  33. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +2 -2
  34. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  35. package/payload/platform/services/claude-session-manager/dist/http-server.js +46 -103
  36. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  37. package/payload/platform/services/claude-session-manager/dist/index.js +40 -10
  38. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  39. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts +35 -0
  40. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.d.ts.map +1 -0
  41. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js +114 -0
  42. package/payload/platform/services/claude-session-manager/dist/rootless-client-audit.js.map +1 -0
  43. package/payload/platform/services/claude-session-manager/package.json +2 -1
  44. package/payload/server/package.json +2 -1
  45. package/payload/server/public/assets/{AdminLoginScreens-C_Qn0UPy.js → AdminLoginScreens-C_h8V0Xs.js} +1 -1
  46. package/payload/server/public/assets/AdminShell-Dqnxpn7U.js +1 -0
  47. package/payload/server/public/assets/{Checkbox-B76SHnPC.js → Checkbox-Bl1WRVGb.js} +1 -1
  48. package/payload/server/public/assets/Transcript-C93Lifnb.js +1 -0
  49. package/payload/server/public/assets/admin-ByoqNaSf.js +1 -0
  50. package/payload/server/public/assets/admin-types-hioowVct.js +1 -0
  51. package/payload/server/public/assets/{arc-JgkiMDpu.js → arc-C6-nr5UV.js} +1 -1
  52. package/payload/server/public/assets/architecture-YZFGNWBL-DdgmZ93B.js +1 -0
  53. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-CyIbl1bD.js → architectureDiagram-Q4EWVU46-DIdho_T6.js} +1 -1
  54. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CkcZVblf.js → blockDiagram-DXYQGD6D-D5rTTBSy.js} +1 -1
  55. package/payload/server/public/assets/{browser-Chmp3Xfv.js → browser-BXY1sNxv.js} +1 -1
  56. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-VByNuobb.js → c4Diagram-AHTNJAMY-pfUVHdmy.js} +1 -1
  57. package/payload/server/public/assets/calendar-CObRTPpU.js +1 -0
  58. package/payload/server/public/assets/channel-Da-UUyqx.js +1 -0
  59. package/payload/server/public/assets/chat-Dq-VHp7a.js +1 -0
  60. package/payload/server/public/assets/chevron-left-DjbK_wdQ.js +1 -0
  61. package/payload/server/public/assets/{chunk-2KRD3SAO-BFwiGcqo.js → chunk-2KRD3SAO-EF6cdj8U.js} +1 -1
  62. package/payload/server/public/assets/{chunk-336JU56O-BMCBU-vf.js → chunk-336JU56O-DE-lcZAc.js} +2 -2
  63. package/payload/server/public/assets/chunk-426QAEUC-B1kFAxo1.js +1 -0
  64. package/payload/server/public/assets/{chunk-4BX2VUAB-RfOeo8_F.js → chunk-4BX2VUAB--ANCzzyn.js} +1 -1
  65. package/payload/server/public/assets/{chunk-4TB4RGXK-B5xbgonc.js → chunk-4TB4RGXK-DTO_X80J.js} +1 -1
  66. package/payload/server/public/assets/{chunk-55IACEB6-K8U55ikJ.js → chunk-55IACEB6-4J3nrz44.js} +1 -1
  67. package/payload/server/public/assets/{chunk-5FUZZQ4R-DgmmsrAc.js → chunk-5FUZZQ4R-jH_eqAHL.js} +1 -1
  68. package/payload/server/public/assets/{chunk-5PVQY5BW-DQ6rtehF.js → chunk-5PVQY5BW-C-qU6p6k.js} +1 -1
  69. package/payload/server/public/assets/{chunk-67CJDMHE-CC2kfpmQ.js → chunk-67CJDMHE-DXxH9Snb.js} +1 -1
  70. package/payload/server/public/assets/{chunk-7N4EOEYR-BClZ5bzw.js → chunk-7N4EOEYR-CCkxuP7A.js} +1 -1
  71. package/payload/server/public/assets/{chunk-AA7GKIK3-adtwAFKX.js → chunk-AA7GKIK3-CyItuM8d.js} +1 -1
  72. package/payload/server/public/assets/{chunk-BSJP7CBP-fKY1UWEd.js → chunk-BSJP7CBP-Bg7gxYNf.js} +1 -1
  73. package/payload/server/public/assets/{chunk-CIAEETIT-knPAMmjG.js → chunk-CIAEETIT-BAXbOqv5.js} +1 -1
  74. package/payload/server/public/assets/{chunk-EDXVE4YY-C7FRKZ5e.js → chunk-EDXVE4YY-vdoErQoc.js} +1 -1
  75. package/payload/server/public/assets/{chunk-ENJZ2VHE-0cgFfv3z.js → chunk-ENJZ2VHE-Cz_z_6GK.js} +1 -1
  76. package/payload/server/public/assets/{chunk-FMBD7UC4-Dm_Hcblb.js → chunk-FMBD7UC4-Ck2jvOgW.js} +1 -1
  77. package/payload/server/public/assets/{chunk-FOC6F5B3-CfCJNXmK.js → chunk-FOC6F5B3-DucR0Qo8.js} +1 -1
  78. package/payload/server/public/assets/{chunk-ICPOFSXX-xeu6qrZj.js → chunk-ICPOFSXX-DolYDD7g.js} +2 -2
  79. package/payload/server/public/assets/{chunk-K5T4RW27-BQDCSuTh.js → chunk-K5T4RW27-FRzx7E5I.js} +1 -1
  80. package/payload/server/public/assets/{chunk-KGLVRYIC-D6fFAgbM.js → chunk-KGLVRYIC-Bb5S4Bmc.js} +1 -1
  81. package/payload/server/public/assets/{chunk-LIHQZDEY-Dskwlc-K.js → chunk-LIHQZDEY-c8VZJ-lU.js} +1 -1
  82. package/payload/server/public/assets/{chunk-ORNJ4GCN-CnHIrJeA.js → chunk-ORNJ4GCN-BAeh-LZ1.js} +1 -1
  83. package/payload/server/public/assets/{chunk-OYMX7WX6-B4bnsPkv.js → chunk-OYMX7WX6-BUg7Kysh.js} +1 -1
  84. package/payload/server/public/assets/chunk-QZHKN3VN-BjE-Glwf.js +1 -0
  85. package/payload/server/public/assets/{chunk-U2HBQHQK-BeEXzOqV.js → chunk-U2HBQHQK-K4r17UTk.js} +1 -1
  86. package/payload/server/public/assets/{chunk-X2U36JSP-D2rpgOqd.js → chunk-X2U36JSP-Byh-CyxC.js} +1 -1
  87. package/payload/server/public/assets/{chunk-XPW4576I-D-yT82Xf.js → chunk-XPW4576I-DC7lhr93.js} +1 -1
  88. package/payload/server/public/assets/{chunk-YZCP3GAM-DqxlLBm1.js → chunk-YZCP3GAM-B2cu7AEP.js} +1 -1
  89. package/payload/server/public/assets/{chunk-ZZ45TVLE-CA_Ep0OM.js → chunk-ZZ45TVLE-DH_p8sFX.js} +1 -1
  90. package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dol3Ogeh.js +1 -0
  91. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-VUMj7gWH.js +1 -0
  92. package/payload/server/public/assets/clone-m7oGrdBC.js +1 -0
  93. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-DWb9mxTa.js → cose-bilkent-S5V4N54A-DeM6JI00.js} +1 -1
  94. package/payload/server/public/assets/{dagre-B3K7jxOZ.js → dagre-3drs3e4F.js} +1 -1
  95. package/payload/server/public/assets/{dagre-KV5264BT-BhiTISRq.js → dagre-KV5264BT-C6S-VUtN.js} +1 -1
  96. package/payload/server/public/assets/data-BXOoK8Ci.js +1 -0
  97. package/payload/server/public/assets/{diagram-5BDNPKRD-D5yBIDpm.js → diagram-5BDNPKRD-BkvA32I3.js} +1 -1
  98. package/payload/server/public/assets/{diagram-G4DWMVQ6-CSTE1S4d.js → diagram-G4DWMVQ6-CVwtRMPB.js} +1 -1
  99. package/payload/server/public/assets/{diagram-MMDJMWI5-D8llv4XY.js → diagram-MMDJMWI5-DJ1JVMbd.js} +1 -1
  100. package/payload/server/public/assets/{diagram-TYMM5635-CITQ7aDM.js → diagram-TYMM5635-rfj-crtX.js} +1 -1
  101. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BjR5nlmv.js → erDiagram-SMLLAGMA-TJPuweS5.js} +1 -1
  102. package/payload/server/public/assets/{flatten-DhYYI_lo.js → flatten-BZkoyJ4e.js} +1 -1
  103. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-Dl6e-E0f.js → flowDiagram-DWJPFMVM-LQWEUjbV.js} +1 -1
  104. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CppkrFAl.js → ganttDiagram-T4ZO3ILL-DdYsxKRw.js} +1 -1
  105. package/payload/server/public/assets/gitGraph-7Q5UKJZL-cJEaiQq4.js +1 -0
  106. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-clV_tnwa.js → gitGraphDiagram-UUTBAWPF-sx8-fokM.js} +1 -1
  107. package/payload/server/public/assets/{graph-DEHmvpNl.js → graph-BCL0fVDH.js} +3 -3
  108. package/payload/server/public/assets/{graph-labels-CYAjU3rz.js → graph-labels-C0nn9nQi.js} +1 -1
  109. package/payload/server/public/assets/{graphlib-Cnk6WL-8.js → graphlib-DixeJdCf.js} +1 -1
  110. package/payload/server/public/assets/info-OMHHGYJF-BUxfOTKa.js +1 -0
  111. package/payload/server/public/assets/infoDiagram-42DDH7IO-CD-d3Rgf.js +2 -0
  112. package/payload/server/public/assets/{isEmpty-DGhq_DBP.js → isEmpty-VRKFml5R.js} +1 -1
  113. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DUqusmP1.js → ishikawaDiagram-UXIWVN3A-BKUCuKhZ.js} +1 -1
  114. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-RMEvGtDJ.js → journeyDiagram-VCZTEJTY-BafZP6pM.js} +1 -1
  115. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BxkwQPCP.js → kanban-definition-6JOO6SKY-DQ4c6wRv.js} +1 -1
  116. package/payload/server/public/assets/{line-CiOuSBlJ.js → line-0_x1Wivu.js} +1 -1
  117. package/payload/server/public/assets/{linear-D22sItNm.js → linear-CZrU-J2V.js} +1 -1
  118. package/payload/server/public/assets/{mermaid-parser.core-kvN8ajT-.js → mermaid-parser.core-DwJ1cyfO.js} +2 -2
  119. package/payload/server/public/assets/{mermaid.core-DKHTTwbg.js → mermaid.core-DnUst0He.js} +3 -3
  120. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CKUJxwdG.js → mindmap-definition-QFDTVHPH-oc-GwerE.js} +1 -1
  121. package/payload/server/public/assets/operator-B_1qYO7m.js +1 -0
  122. package/payload/server/public/assets/{ordinal-CibZU3PM.js → ordinal-v4prQcuq.js} +1 -1
  123. package/payload/server/public/assets/packet-4T2RLAQJ-B9U_2639.js +1 -0
  124. package/payload/server/public/assets/page-BQEl7aZO.js +1 -0
  125. package/payload/server/public/assets/page-Dck23z9U.js +32 -0
  126. package/payload/server/public/assets/pdf-render-NQmflZ7K.js +11 -0
  127. package/payload/server/public/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  128. package/payload/server/public/assets/pie-ZZUOXDRM-Glh-lJOT.js +1 -0
  129. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CRMabeJN.js → pieDiagram-DEJITSTG-BnBtQNHM.js} +1 -1
  130. package/payload/server/public/assets/public-BzvwPHM0.js +1 -0
  131. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DcrlhDuj.js → quadrantDiagram-34T5L4WZ-CkNqtzHI.js} +1 -1
  132. package/payload/server/public/assets/radar-PYXPWWZC-CTSgob8w.js +1 -0
  133. package/payload/server/public/assets/{reduce-Da0RBva0.js → reduce-0Mm1v1Rr.js} +1 -1
  134. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DP231rM_.js → requirementDiagram-MS252O5E-D7qc27KT.js} +1 -1
  135. package/payload/server/public/assets/{rotate-ccw-K1xZvf3B.js → rotate-ccw-CHep9vl5.js} +1 -1
  136. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6--HM5_dC8.js → sankeyDiagram-XADWPNL6-2Y4sqRY7.js} +1 -1
  137. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BsiVmDR9.js → sequenceDiagram-FGHM5R23-hXOiSHuT.js} +1 -1
  138. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-Cjdhqdjh.js → stateDiagram-FHFEXIEX-Dq4hhkLA.js} +1 -1
  139. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DTj9o2vZ.js +1 -0
  140. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Cf5xPmSA.js → timeline-definition-GMOUNBTQ-Dn9wBUEw.js} +1 -1
  141. package/payload/server/public/assets/treeView-SZITEDCU-CXApkW1l.js +1 -0
  142. package/payload/server/public/assets/treemap-W4RFUUIX-DzvTXAcT.js +1 -0
  143. package/payload/server/public/assets/useSubAccountSwitcher-B1Kf32uG.js +9 -0
  144. package/payload/server/public/assets/{useSubAccountSwitcher-B44qYovc.css → useSubAccountSwitcher-CfI3J2IX.css} +1 -1
  145. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-PCor55Sw.js → vennDiagram-DHZGUBPP-BKVHJiIt.js} +1 -1
  146. package/payload/server/public/assets/wardley-RL74JXVD-BExXV4oI.js +1 -0
  147. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-bokCHsim.js → wardleyDiagram-NUSXRM2D-Bn9vvpdK.js} +1 -1
  148. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-B2YnsUoQ.js → xychartDiagram-5P7HB3ND-FIoMdPB3.js} +1 -1
  149. package/payload/server/public/browser.html +5 -4
  150. package/payload/server/public/calendar.html +6 -4
  151. package/payload/server/public/chat.html +11 -8
  152. package/payload/server/public/data.html +8 -5
  153. package/payload/server/public/graph.html +9 -7
  154. package/payload/server/public/index.html +11 -9
  155. package/payload/server/public/operator.html +13 -10
  156. package/payload/server/public/public.html +10 -8
  157. package/payload/server/server.js +150 -21
  158. package/payload/server/public/assets/AdminShell-iHdYhspw.js +0 -1
  159. package/payload/server/public/assets/admin-RTEhnrPk.js +0 -1
  160. package/payload/server/public/assets/admin-types-DJoj6VJv.js +0 -1
  161. package/payload/server/public/assets/architecture-YZFGNWBL-Dh5BzVzk.js +0 -1
  162. package/payload/server/public/assets/calendar-CjE7hiqV.js +0 -1
  163. package/payload/server/public/assets/channel-CFHc3PQ7.js +0 -1
  164. package/payload/server/public/assets/chat-DiKmCMO1.js +0 -1
  165. package/payload/server/public/assets/chunk-426QAEUC-De9_O1hi.js +0 -1
  166. package/payload/server/public/assets/chunk-QZHKN3VN-COwe7oXR.js +0 -1
  167. package/payload/server/public/assets/classDiagram-6PBFFD2Q-wjL65qaU.js +0 -1
  168. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D_EIplVd.js +0 -1
  169. package/payload/server/public/assets/clone-CEczy_Ef.js +0 -1
  170. package/payload/server/public/assets/data-5PpEsUR3.js +0 -1
  171. package/payload/server/public/assets/gitGraph-7Q5UKJZL-KHAc7IxG.js +0 -1
  172. package/payload/server/public/assets/info-OMHHGYJF-C6nm3sYf.js +0 -1
  173. package/payload/server/public/assets/infoDiagram-42DDH7IO-DhwCejsr.js +0 -2
  174. package/payload/server/public/assets/operator-BDTnuQhB.js +0 -1
  175. package/payload/server/public/assets/packet-4T2RLAQJ-z-APhyNH.js +0 -1
  176. package/payload/server/public/assets/page-D8FFXoI9.js +0 -1
  177. package/payload/server/public/assets/page-HeiBebPq.js +0 -32
  178. package/payload/server/public/assets/pie-ZZUOXDRM-D_HVZjir.js +0 -1
  179. package/payload/server/public/assets/public-BIOxrxZK.js +0 -1
  180. package/payload/server/public/assets/radar-PYXPWWZC-DtKBIb3o.js +0 -1
  181. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-6v4alr7n.js +0 -1
  182. package/payload/server/public/assets/treeView-SZITEDCU-DbXWs029.js +0 -1
  183. package/payload/server/public/assets/treemap-W4RFUUIX-DUGgqAs9.js +0 -1
  184. package/payload/server/public/assets/useSubAccountSwitcher-DPEkYkYd.js +0 -9
  185. package/payload/server/public/assets/wardley-RL74JXVD-Cjn85RON.js +0 -1
  186. /package/payload/server/public/assets/{_baseFor-BFYpZGkN.js → _baseFor-C0Y1mc3g.js} +0 -0
  187. /package/payload/server/public/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
  188. /package/payload/server/public/assets/{cytoscape.esm-DqlHsaog.js → cytoscape.esm-Cn5b4Cdu.js} +0 -0
  189. /package/payload/server/public/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
  190. /package/payload/server/public/assets/{dist-BMqkacmt.js → dist-Dlks0Bfw.js} +0 -0
  191. /package/payload/server/public/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
  192. /package/payload/server/public/assets/{katex-pVJiI_rc.js → katex-BFwFoEts.js} +0 -0
  193. /package/payload/server/public/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
  194. /package/payload/server/public/assets/{preload-helper-B_l5jfgx.js → preload-helper-C5gCWwwF.js} +0 -0
  195. /package/payload/server/public/assets/{rough.esm-nHaDi0Kw.js → rough.esm-DsHEmXUZ.js} +0 -0
  196. /package/payload/server/public/assets/{src-CsEKjNXD.js → src-Bv7ZWAzF.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import type { CreatedBy } from "../../../../../lib/graph-write/dist/index.js";
1
2
  export interface AccountSummary {
2
3
  accountId: string;
3
4
  role: string;
@@ -43,6 +44,53 @@ export declare function provisionClientAccount(opts: {
43
44
  accountId: string;
44
45
  usersFile: string;
45
46
  }): string;
47
+ /** Minimal Neo4j session surface the root seed needs — the admin plugin's
48
+ * `getSession()` satisfies it; tests pass an in-memory fake. */
49
+ export interface SeedSessionLike {
50
+ run(query: string, params?: Record<string, unknown>): Promise<{
51
+ records: Array<{
52
+ get(key: string): unknown;
53
+ }>;
54
+ }>;
55
+ }
56
+ /** The provenance the root seed represents — matches the properties the cypher
57
+ * below stamps (`createdByAgent='system'`, `createdBySource='installer'`), the
58
+ * same shape `seed-neo4j.sh` writes for the house root. `agent:"system"` is the
59
+ * convention `checkLabelOriginGate` exempts. The seed runs as raw cypher through
60
+ * the admin session (not the memory-write chokepoint), so the gate never
61
+ * actually evaluates it in production; this constant is not the write's
62
+ * provenance carrier — it exists so a unit test can assert the gate *would*
63
+ * permit this origin. */
64
+ export declare const SEED_ORIGIN: CreatedBy;
65
+ /** Task 1359 — seed a client account's graph root: a bare :LocalBusiness{accountId}
66
+ * plus its owner :AdminUser (fresh per-account userId) joined by :ADMIN_OF, in
67
+ * one statement, then read both node existences back. Without this a created
68
+ * client account is rootless: `checkGraphWriteGate` refuses every non-root
69
+ * write and `checkLabelOriginGate` refuses the agent's attempt to create the
70
+ * root, so the graph is unpopulatable.
71
+ *
72
+ * Raw cypher through the admin session bypasses the memory-write gates by
73
+ * construction (they guard only the memory-write chokepoint), mirroring
74
+ * `seed-neo4j.sh`. The owner AdminUser carries a fresh `ownerUserId` because
75
+ * AdminUser is MERGE-keyed on `userId` and COALESCEs `accountId`; reusing the
76
+ * install owner's userId would keep `accountId=house` and leave the gate's
77
+ * `MATCH (au:AdminUser {accountId: <client>})` empty. Single-shot per account:
78
+ * MERGE on `LocalBusiness.accountId` (a UNIQUE key) makes a replay against the
79
+ * same accountId safe, but it is NOT replay-idempotent under a different
80
+ * `ownerUserId` (that would MERGE a second owner AdminUser); the sole caller
81
+ * mints a new accountId per create and never replays. Returns the two
82
+ * read-back booleans; the caller treats `businessSeeded && adminSeeded` as the
83
+ * verified post-condition. */
84
+ export declare function seedAccountGraphRoot(args: {
85
+ session: SeedSessionLike;
86
+ accountId: string;
87
+ ownerUserId: string;
88
+ ownerName: string;
89
+ createdAt: string;
90
+ }): Promise<{
91
+ businessSeeded: boolean;
92
+ adminSeeded: boolean;
93
+ }>;
46
94
  /** Cypher for the account-scoped node count and the irreversible purge. The
47
95
  * purge deletes every node carrying the accountId (account scope is a property,
48
96
  * never an edge — see CLAUDE.md), detaching its relationships. */
@@ -1 +1 @@
1
- {"version":3,"file":"account-lifecycle.d.ts","sourceRoot":"","sources":["../../src/tools/account-lifecycle.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;oCAEoC;AACpC,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,EAAE,CAoBzE;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvE;AAED;;+DAE+D;AAC/D,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAE3E;AAED;;;;;+DAK+D;AAC/D,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED;;;;0EAI0E;AAC1E,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAO7F;AAED;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,GACT;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAO3C;AAED;;;yBAGyB;AACzB,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAoBT;AAED;;mEAEmE;AACnE,eAAO,MAAM,yBAAyB,2DAA2D,CAAC;AAClG,eAAO,MAAM,oBAAoB,sDAAsD,CAAC"}
1
+ {"version":3,"file":"account-lifecycle.d.ts","sourceRoot":"","sources":["../../src/tools/account-lifecycle.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8CAA8C,CAAC;AAI9E,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;oCAEoC;AACpC,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,EAAE,CAoBzE;AAED,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvE;AAED;;+DAE+D;AAC/D,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAE3E;AAED;;;;;+DAK+D;AAC/D,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED;;;;0EAI0E;AAC1E,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,CAO7F;AAED;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,GACT;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAO3C;AAED;;;yBAGyB;AACzB,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAoBT;AAED;iEACiE;AACjE,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAClH;AAED;;;;;;;0BAO0B;AAC1B,eAAO,MAAM,WAAW,EAAE,SAAoD,CAAC;AAE/E;;;;;;;;;;;;;;;;;;+BAkB+B;AAC/B,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,CAyB7D;AAED;;mEAEmE;AACnE,eAAO,MAAM,yBAAyB,2DAA2D,CAAC;AAClG,eAAO,MAAM,oBAAoB,sDAAsD,CAAC"}
@@ -109,6 +109,57 @@ export function provisionClientAccount(opts) {
109
109
  });
110
110
  return accountDir;
111
111
  }
112
+ /** The provenance the root seed represents — matches the properties the cypher
113
+ * below stamps (`createdByAgent='system'`, `createdBySource='installer'`), the
114
+ * same shape `seed-neo4j.sh` writes for the house root. `agent:"system"` is the
115
+ * convention `checkLabelOriginGate` exempts. The seed runs as raw cypher through
116
+ * the admin session (not the memory-write chokepoint), so the gate never
117
+ * actually evaluates it in production; this constant is not the write's
118
+ * provenance carrier — it exists so a unit test can assert the gate *would*
119
+ * permit this origin. */
120
+ export const SEED_ORIGIN = { agent: "system", source: "installer" };
121
+ /** Task 1359 — seed a client account's graph root: a bare :LocalBusiness{accountId}
122
+ * plus its owner :AdminUser (fresh per-account userId) joined by :ADMIN_OF, in
123
+ * one statement, then read both node existences back. Without this a created
124
+ * client account is rootless: `checkGraphWriteGate` refuses every non-root
125
+ * write and `checkLabelOriginGate` refuses the agent's attempt to create the
126
+ * root, so the graph is unpopulatable.
127
+ *
128
+ * Raw cypher through the admin session bypasses the memory-write gates by
129
+ * construction (they guard only the memory-write chokepoint), mirroring
130
+ * `seed-neo4j.sh`. The owner AdminUser carries a fresh `ownerUserId` because
131
+ * AdminUser is MERGE-keyed on `userId` and COALESCEs `accountId`; reusing the
132
+ * install owner's userId would keep `accountId=house` and leave the gate's
133
+ * `MATCH (au:AdminUser {accountId: <client>})` empty. Single-shot per account:
134
+ * MERGE on `LocalBusiness.accountId` (a UNIQUE key) makes a replay against the
135
+ * same accountId safe, but it is NOT replay-idempotent under a different
136
+ * `ownerUserId` (that would MERGE a second owner AdminUser); the sole caller
137
+ * mints a new accountId per create and never replays. Returns the two
138
+ * read-back booleans; the caller treats `businessSeeded && adminSeeded` as the
139
+ * verified post-condition. */
140
+ export async function seedAccountGraphRoot(args) {
141
+ const { session, accountId, ownerUserId, ownerName, createdAt } = args;
142
+ const res = await session.run(`MERGE (lb:LocalBusiness {accountId: $accountId})
143
+ ON CREATE SET lb.createdBySource = 'installer',
144
+ lb.createdByAgent = 'system',
145
+ lb.createdAt = datetime()
146
+ MERGE (au:AdminUser {userId: $ownerUserId})
147
+ ON CREATE SET au.accountId = $accountId,
148
+ au.role = 'owner',
149
+ au.name = $ownerName,
150
+ au.createdAt = $createdAt
151
+ ON MATCH SET au.accountId = COALESCE(au.accountId, $accountId)
152
+ MERGE (au)-[r:ADMIN_OF]->(lb)
153
+ ON CREATE SET r.role = 'owner', r.grantedAt = $createdAt
154
+ WITH lb.accountId AS acct
155
+ RETURN COUNT { MATCH (b:LocalBusiness {accountId: acct}) } > 0 AS hasBusiness,
156
+ COUNT { MATCH (a:AdminUser {accountId: acct}) } > 0 AS hasAdmin`, { accountId, ownerUserId, ownerName, createdAt });
157
+ const rec = res.records[0];
158
+ return {
159
+ businessSeeded: Boolean(rec?.get("hasBusiness")),
160
+ adminSeeded: Boolean(rec?.get("hasAdmin")),
161
+ };
162
+ }
112
163
  /** Cypher for the account-scoped node count and the irreversible purge. The
113
164
  * purge deletes every node carrying the accountId (account scope is a property,
114
165
  * never an edge — see CLAUDE.md), detaching its relationships. */
@@ -1 +1 @@
1
- {"version":3,"file":"account-lifecycle.js","sourceRoot":"","sources":["../../src/tools/account-lifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"account-lifecycle.js","sourceRoot":"","sources":["../../src/tools/account-lifecycle.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:05d8fd7ac87ffe3c93e50e42ae7e55b365ca761ae9693eec9fd23d34f7fc98aa
4
+ content-hash: sha256:6ab87c68af715d29cae2a3fa1273c434fb8c88cdbe96eb86e0c7216895625085
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2151,7 +2151,11 @@ Source: https://docs.getmaxy.com/attachments.md
2151
2151
 
2152
2152
  # Admin Chat Attachments
2153
2153
 
2154
- What you can drag-and-drop into the admin chat window, what happens to each file, and the size caps.
2154
+ What you can drag-and-drop or paste into the admin chat window, what happens to each file, and the size caps.
2155
+
2156
+ ## How files get in
2157
+
2158
+ You can drag-and-drop a file, pick one with the paperclip, or paste. All three routes stage the file the same way and enforce the same caps. Pasting a file you copied from your file manager (or another app) keeps its real name and type, so `report.pdf` stays `report.pdf`. Pasting a screenshot or a raw clipboard image has no filename to keep, so it is staged under a generated unique name like `pasted-image-1700000000000.png`. Pasting plain text still types the text into the message box as before.
2155
2159
 
2156
2160
  ## Accepted file types
2157
2161
 
@@ -2533,7 +2537,7 @@ either is a regression.
2533
2537
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
2534
2538
  | `/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) |
2535
2539
  | `/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. | `GET /` |
2536
- | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Forwards the caller's canonical `accountId` (`getAccountIdForSession(cacheKey)`) as `?accountId=<uuid>` on both calls. The manager's fs-watcher is armed only on the house account's project slug, so a session under any other account has no watcher row; without the forwarded accountId the manager 404s a live cross-account session on stop (silently swallowed by this route's `stop.status !== 404` tolerance) and its transcript on delete. With it, the manager derives the project slug from the account (`<accountsRoot>/<accountId>` slug), gates on file existence alone, and (stop) reads the on-disk scope record to kill a cross-account scope unit that survived a manager restart. Absent accountId the manager keeps its watcher-row fallback. The sibling `/session-stop` (the standalone End control) forwards `accountId` identically, for the same reason. | `POST /` |
2540
+ | `/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 /` |
2537
2541
  | `/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 /` |
2538
2542
  | `/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 /` |
2539
2543
 
@@ -2799,13 +2803,19 @@ authoritative. This section names the surfaces and what backs each.
2799
2803
  > **Admin variant only.** Every surface below belongs to the `admin` shell
2800
2804
  > variant. The **operator** variant (`operator.<host>`) mounts no sidebar at
2801
2805
  > all: its dashboard is the persistent admin webchat at `/` and the data
2802
- > browser at `/data`, plus a read-only **Conversations** reader reached from a
2803
- > dynamic burger item — header order Chat → Conversations → Data → Log out, in
2804
- > a single-column `.platform.platform-operator` grid. The operator never sees
2805
- > the sessions list or the nav rows; the Conversations reader lists the agent's
2806
- > inbound channel conversations (WhatsApp, Telegram, public webchat) read-only
2807
- > via the same admin-gated reader routes, never the operator's own session. The
2808
- > operator surface is documented in full in the internal doc
2806
+ > browser at `/data`, plus a **Conversations** item reached from a dynamic
2807
+ > burger item — header order Chat → Conversations → Data → Log out, in a
2808
+ > single-column `.platform.platform-operator` grid. On the operator **chat**
2809
+ > surface the Conversations item opens the same in-chat session
2810
+ > switcher the admin `/chat` surface uses (`ChatSurface`'s `SessionList` flyout):
2811
+ > the operator's own admin webchat conversations, newest-first, each resuming
2812
+ > live via `/chat?session=<id>`, with rename / archive / delete / end / new — not
2813
+ > a read-only reader. The item gates on the flyout handler being wired, exactly
2814
+ > like the admin and lite items. The read-only inbound-channel **Conversations**
2815
+ > reader (WhatsApp, Telegram, public webchat, via the admin-gated
2816
+ > `/api/whatsapp-reader/conversations` route) remains on `/data`'s Conversations
2817
+ > item and in the admin Sidebar; it is no longer reachable from the operator chat
2818
+ > surface. The operator surface is documented in full in the internal doc
2809
2819
  > `maxy-code/.docs/operator-dashboard.md`.
2810
2820
  >
2811
2821
  > **Operator `/chat`.** The reduced operator chrome renders on
@@ -2823,28 +2833,33 @@ authoritative. This section names the surfaces and what backs each.
2823
2833
  > (`.admin-shell-root:has(> .platform-operator) > .admin-header`), the chat-column
2824
2834
  > clamp (`.platform-operator > .webchat-page`), and the `/data` clamp
2825
2835
  > (`.data-main`) all read it, so logo and burger align to the panel edges on both
2826
- > surfaces. The admin shell carries no `--chat-measure` consumer and stays
2827
- > edge-to-edge; the public webchat (`.public-surface`) inherits the same value.
2836
+ > surfaces. In the full admin shell the one `--chat-measure` consumer is the
2837
+ > main-column WhatsApp/Telegram viewer (`.platform > .wa-reader`), clamped and
2838
+ > centred so its wallpaper reads as a bounded column, not an edge-to-edge
2839
+ > surface; the rest of the admin shell stays edge-to-edge. The public webchat
2840
+ > (`.public-surface`) inherits the same value.
2828
2841
  >
2829
- > **Conversations item.** The dynamic Conversations burger item appears on
2830
- > **both** the operator chat surface and `/data` whenever ≥1 reviewable
2831
- > conversation exists (the count/channels are fetched per surface from
2832
- > `/whatsapp-reader/conversations` with the operator's session key). On `/data`
2833
- > selecting it renders the same read-only reader in place (the file browser is
2834
- > swapped for it; Back returns). The flyout-open signal
2835
- > `[operator-ui] op=conversations-menu present=<bool> channels=[…] count=<n>`
2836
- > fires on `/data` too `present=false` there while the chat surface shows the
2837
- > item is the props-not-wired signature.
2842
+ > **Conversations item.** The dynamic Conversations burger item gates on the
2843
+ > host wiring its open-handler (like the admin and lite items), not on a channel
2844
+ > count. On the operator **chat** surface the handler is always wired, so the
2845
+ > item is always present and opens the live session switcher (above). On `/data`
2846
+ > the handler is wired only when ≥1 reviewable inbound conversation exists
2847
+ > (fetched from `/whatsapp-reader/conversations` with the operator's session
2848
+ > key), and selecting it renders the read-only reader in place (the file browser
2849
+ > is swapped for it; Back returns) so `/data` keeps its hide-when-empty
2850
+ > behaviour. The flyout-open signal `[operator-ui] op=conversations-menu
2851
+ > present=<bool>` fires for the operator variant on both surfaces; `present=false`
2852
+ > while the operator expects the item is the handler-not-wired signature.
2838
2853
 
2839
2854
  | Surface | What it does | Backed by |
2840
2855
  |---|---|---|
2841
2856
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
2842
2857
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
2843
2858
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so Maxy reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
2844
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
2859
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). **Clicking the row's body** (live-dot, title, id, or timestamp) opens that same `/chat?session=<id>` as the chat-launch action. The main area stays a `<div>` (it wraps the inline rename input, which a `<button>` may not contain) carrying `role="button"`, `tabIndex`, an "Open <title> in webchat" aria-label, and Enter/Space activation, styled with `conv-main-clickable` for pointer + hover parity with the channel-row button. The click and keyboard handlers no-op while the row is being renamed, so an in-progress edit is preserved; the action icons and the `⋯` overflow menu are a sibling cluster running their own handlers, so activating them never triggers the row-body open. At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
2845
2860
  | Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
2846
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). Server logs `[wa-reader] op=list accountId=<id> conversations=<n> adminScope=<n> publicScope=<n>` per list build (`conversations=0` on an account with a non-empty store dir is the drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/whatsapp-reader/conversations` |
2847
- | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /whatsapp-reader/directives?sessionId=`, bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
2861
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/whatsapp-reader/conversations` |
2862
+ | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /whatsapp-reader/directives?sessionId=`, bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. This viewer renders **delivered messages only** (both WhatsApp store rows and Telegram session rows, via `forceDeliveredOnly`) with **no per-viewer toggle** — the former "Messages only" pill is retired; the admin `/chat` composer's own messages-only control is unaffected. The panel — wallpaper included — is **bounded to `--chat-measure` and centred** (`.platform > .wa-reader`), so it reads as a column the same width as `/data`, not an edge-to-edge beige surface. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
2848
2863
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
2849
2864
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
2850
2865
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
@@ -33,15 +33,18 @@ If the site captures form data, add the `[[d1_databases]]` binding from `d1-data
33
33
 
34
34
  ## 3. Load the reused deploy token
35
35
 
36
- A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). Load the stable per-scope token minting it once only if absent per `api.md` § Provisioning and reusing a stable per-scope token:
36
+ A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:
37
37
 
38
38
  ```bash
39
+ KEY=$(bash platform/plugins/cloudflare/bin/cf-token.sh pages "${SECRETS_DIR}/cloudflare.env")
39
40
  set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
40
- : "${CLOUDFLARE_API_TOKEN:?credentials not loaded}"; : "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded}"
41
- # PAGES_D1 = the reused per-scope token (Pages Edit, + D1 Edit if the site uses D1),
42
- # loaded from cloudflare.env and minted once if absent see api.md § Provisioning and reusing.
41
+ : "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded}"
42
+ PAGES_D1="${!KEY}" # the active Pages(+D1) per-scope token NOT the sourced CLOUDFLARE_API_TOKEN (that is the master)
43
+ : "${PAGES_D1:?per-scope token not resolvedre-run cf-token.sh and read its output}"
43
44
  ```
44
45
 
46
+ A `10000` here is a token-selection symptom, not a permissions wall: it means the master reached `wrangler`. Re-run the helper line and retry with `${PAGES_D1}`; never fall back to a manual step on the strength of a `10000`. The mint-once, reuse, prefix-routing, and redaction details live in `api.md` § Provisioning and reusing a stable per-scope token; this command is self-contained and does not depend on reading that section first.
47
+
45
48
  ## 4. Deploy
46
49
 
47
50
  ```bash
@@ -9,7 +9,9 @@ Run by the admin/main session, the operator-infra surface that holds the `Bash`
9
9
 
10
10
  This is the entry point for every Cloudflare task on the install. Pick the operation class, read the matching reference, run the command via Bash, relay the literal output, and prove the outcome with a live signal. There are no Cloudflare MCP tools; the plugin registers none. The agent never browser-automates the dashboard — the operator clicks where a click is genuinely required.
11
11
 
12
- | You want to… | Read | Outcome contract |
12
+ **How to open every `references/…` file named in this skill.** These reference files live at the plugin root (`platform/plugins/cloudflare/references/`), one directory above this skill. They are not children of the directory this skill announces as its base. Read each one with the `plugin-read` tool, which resolves the path plugin-root-relative: `plugin-read` with `file="references/api.md"` returns the file. A plain `Read` of `references/api.md` against this skill's announced base directory resolves to `.../skills/cloudflare/references/api.md`, which does not exist and returns "File does not exist". That miss is not a reason to skip the reference, to substitute a non-canonical file, or to improvise the command from memory. Use `plugin-read` for every `references/…` mention below, the same way the sibling `site-deploy` and `calendar-site` skills do.
13
+
14
+ | You want to… | Read via `plugin-read` | Outcome contract |
13
15
  |---|---|---|
14
16
  | Stand up / diagnose / reset a tunnel so a hostname is reachable | `references/manual-setup.md` | external `curl -I https://<hostname>` → `200` |
15
17
  | Switch accounts, edit an apex CNAME by hand, or other dashboard-only clicks | `references/dashboard-guide.md` | the click-path completed in the operator's browser |
@@ -27,19 +29,20 @@ This is the entry point for every Cloudflare task on the install. Pick the opera
27
29
 
28
30
  Neither the master token nor any per-scope token is ever written into a project tree, committed, or echoed into chat — the per-scope tokens persist only in the account-scoped secrets file, never in a deployable project tree.
29
31
 
30
- ### Token operations: run the helper first (binding)
32
+ ### Resolve the per-scope token before any `wrangler` / API call (binding)
31
33
 
32
- Before **any** Cloudflare API token operation (verify, list, mint, reuse), resolve the per-scope token with the deterministic helper, never a hand-built `curl`:
34
+ This binding governs **every** Pages, D1, DNS, or Access operation, not only calls you think of as "token operations." Before the first `wrangler pages …`, `wrangler d1 …`, or Cloudflare-API `curl` of a task, resolve the per-scope token with the deterministic helper and pass **that** token to the command:
33
35
 
34
36
  ```bash
35
37
  KEY=$(bash platform/plugins/cloudflare/bin/cf-token.sh <scope> <secrets-file>) # scope: pages | d1 | dns | access
36
38
  set -a; . <secrets-file>; set +a
37
- # the active, persisted per-scope token is now in the variable named by $KEY
39
+ # ${!KEY} now holds the active, persisted per-scope token. Pass it explicitly:
40
+ CLOUDFLARE_API_TOKEN="${!KEY}" wrangler pages project list
38
41
  ```
39
42
 
40
- The helper owns the one decision the false-negative failure turns on: it reads the master's `cfat_`/`cfut_` prefix and routes verify, `permission_groups`, and mint to the matching endpoint family, mints once if absent (no expiry), verifies through the transient post-mint window, persists to the secrets file, and prints only the secrets key name. It never prints a token value.
43
+ **The antipattern this exists to stop.** Sourcing the secrets file loads the **master** token into `CLOUDFLARE_API_TOKEN`. If you then run `wrangler pages …` (or a Cloudflare-API `curl`) with the environment as-sourced, you hand the **master** to the tool, not the per-scope token. The master is the wrong token for these endpoints, so Cloudflare rejects the call, and the rejection reads like a permissions wall when it is really a token-selection mistake. `. <secrets-file>` immediately followed by `wrangler pages …` is the exact sequence that trips this; the helper line above is what prevents it. Never let `CLOUDFLARE_API_TOKEN` reach `wrangler`/`curl` still holding the sourced master.
41
44
 
42
- A raw `curl` to a token endpoint chosen **before** the master's prefix is established is out of bounds. The wrong endpoint returns a stable false-negative that reads as "cannot mint" but is not: a `cfat_` master at `/user/tokens/verify` returns `Invalid API Token`, and a `cfut_` master at any `/accounts/{id}/...` endpoint returns `9109`. If you see either from a token probe, the cause is the endpoint, not the token. Stop and run the helper. Doing the verify or mint by hand is permitted only after reading the `references/api.md` section on routing token endpoints by prefix, and routing by the established prefix.
45
+ **A `10000` / `Invalid API Token` / `9109` from a `wrangler` or API call is a token-selection symptom, never "this token lacks permission."** The recovery is always the same: run `cf-token.sh <scope> <secrets-file>` and re-issue the command with the resolved per-scope token. Concretely: a just-sourced master handed to `wrangler pages …` returns `Authentication error [code: 10000]`; a `cfat_` master at `/user/tokens/verify` returns `Invalid API Token`; a `cfut_` master at any `/accounts/{id}/...` endpoint returns `9109`. None of these means "cannot deploy" or "lacks Pages/DNS permission." Do not fall back to a manual DNS record, a tunnel workaround, or an operator ask on the strength of one of these codes — run the helper and retry. The helper reads the master's `cfat_`/`cfut_` prefix, routes verify/`permission_groups`/mint to the matching endpoint family, mints once if absent (no expiry), verifies through the transient post-mint window, persists to the secrets file, and prints only the secrets key name; it never prints a token value. Doing verify or mint by hand is permitted only after reading the `references/api.md` section on routing token endpoints by prefix, and routing by the established prefix.
43
46
 
44
47
  ## Outcome contracts — what "done" means
45
48
 
@@ -57,7 +57,7 @@ either is a regression.
57
57
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
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. | `GET /` |
60
- | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Forwards the caller's canonical `accountId` (`getAccountIdForSession(cacheKey)`) as `?accountId=<uuid>` on both calls. The manager's fs-watcher is armed only on the house account's project slug, so a session under any other account has no watcher row; without the forwarded accountId the manager 404s a live cross-account session on stop (silently swallowed by this route's `stop.status !== 404` tolerance) and its transcript on delete. With it, the manager derives the project slug from the account (`<accountsRoot>/<accountId>` slug), gates on file existence alone, and (stop) reads the on-disk scope record to kill a cross-account scope unit that survived a manager restart. Absent accountId the manager keeps its watcher-row fallback. The sibling `/session-stop` (the standalone End control) forwards `accountId` identically, for the same reason. | `POST /` |
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
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 /` |
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
 
@@ -323,13 +323,19 @@ authoritative. This section names the surfaces and what backs each.
323
323
  > **Admin variant only.** Every surface below belongs to the `admin` shell
324
324
  > variant. The **operator** variant (`operator.<host>`) mounts no sidebar at
325
325
  > all: its dashboard is the persistent admin webchat at `/` and the data
326
- > browser at `/data`, plus a read-only **Conversations** reader reached from a
327
- > dynamic burger item — header order Chat → Conversations → Data → Log out, in
328
- > a single-column `.platform.platform-operator` grid. The operator never sees
329
- > the sessions list or the nav rows; the Conversations reader lists the agent's
330
- > inbound channel conversations (WhatsApp, Telegram, public webchat) read-only
331
- > via the same admin-gated reader routes, never the operator's own session. The
332
- > operator surface is documented in full in the internal doc
326
+ > browser at `/data`, plus a **Conversations** item reached from a dynamic
327
+ > burger item — header order Chat → Conversations → Data → Log out, in a
328
+ > single-column `.platform.platform-operator` grid. On the operator **chat**
329
+ > surface the Conversations item opens the same in-chat session
330
+ > switcher the admin `/chat` surface uses (`ChatSurface`'s `SessionList` flyout):
331
+ > the operator's own admin webchat conversations, newest-first, each resuming
332
+ > live via `/chat?session=<id>`, with rename / archive / delete / end / new — not
333
+ > a read-only reader. The item gates on the flyout handler being wired, exactly
334
+ > like the admin and lite items. The read-only inbound-channel **Conversations**
335
+ > reader (WhatsApp, Telegram, public webchat, via the admin-gated
336
+ > `/api/whatsapp-reader/conversations` route) remains on `/data`'s Conversations
337
+ > item and in the admin Sidebar; it is no longer reachable from the operator chat
338
+ > surface. The operator surface is documented in full in the internal doc
333
339
  > `maxy-code/.docs/operator-dashboard.md`.
334
340
  >
335
341
  > **Operator `/chat`.** The reduced operator chrome renders on
@@ -347,28 +353,33 @@ authoritative. This section names the surfaces and what backs each.
347
353
  > (`.admin-shell-root:has(> .platform-operator) > .admin-header`), the chat-column
348
354
  > clamp (`.platform-operator > .webchat-page`), and the `/data` clamp
349
355
  > (`.data-main`) all read it, so logo and burger align to the panel edges on both
350
- > surfaces. The admin shell carries no `--chat-measure` consumer and stays
351
- > edge-to-edge; the public webchat (`.public-surface`) inherits the same value.
356
+ > surfaces. In the full admin shell the one `--chat-measure` consumer is the
357
+ > main-column WhatsApp/Telegram viewer (`.platform > .wa-reader`), clamped and
358
+ > centred so its wallpaper reads as a bounded column, not an edge-to-edge
359
+ > surface; the rest of the admin shell stays edge-to-edge. The public webchat
360
+ > (`.public-surface`) inherits the same value.
352
361
  >
353
- > **Conversations item.** The dynamic Conversations burger item appears on
354
- > **both** the operator chat surface and `/data` whenever ≥1 reviewable
355
- > conversation exists (the count/channels are fetched per surface from
356
- > `/whatsapp-reader/conversations` with the operator's session key). On `/data`
357
- > selecting it renders the same read-only reader in place (the file browser is
358
- > swapped for it; Back returns). The flyout-open signal
359
- > `[operator-ui] op=conversations-menu present=<bool> channels=[…] count=<n>`
360
- > fires on `/data` too `present=false` there while the chat surface shows the
361
- > item is the props-not-wired signature.
362
+ > **Conversations item.** The dynamic Conversations burger item gates on the
363
+ > host wiring its open-handler (like the admin and lite items), not on a channel
364
+ > count. On the operator **chat** surface the handler is always wired, so the
365
+ > item is always present and opens the live session switcher (above). On `/data`
366
+ > the handler is wired only when ≥1 reviewable inbound conversation exists
367
+ > (fetched from `/whatsapp-reader/conversations` with the operator's session
368
+ > key), and selecting it renders the read-only reader in place (the file browser
369
+ > is swapped for it; Back returns) so `/data` keeps its hide-when-empty
370
+ > behaviour. The flyout-open signal `[operator-ui] op=conversations-menu
371
+ > present=<bool>` fires for the operator variant on both surfaces; `present=false`
372
+ > while the operator expects the item is the handler-not-wired signature.
362
373
 
363
374
  | Surface | What it does | Backed by |
364
375
  |---|---|---|
365
376
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
366
377
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
367
378
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. **The "Projects" row is brand-aware:** its visible term and the graph label it filters to both come from the injected `window.__BRAND__.primaryContainer` (`brand.json` → `server/index.ts` inject → `brand.ts` `brandPrimaryContainer()`), so Maxy reads "Projects" → `:Project`, SiteDesk "Jobs" → `:Job`, Real Agent "Properties" → `:Property`. A brand JSON missing `primaryContainer` falls back to Project/Projects (server logs `[brand] op=primaryContainer-default reason=absent brand=<hostname>`; the populated case logs `[brand] op=inject primaryContainer=<label>/<term>`). | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
368
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
379
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). **Clicking the row's body** (live-dot, title, id, or timestamp) opens that same `/chat?session=<id>` as the chat-launch action. The main area stays a `<div>` (it wraps the inline rename input, which a `<button>` may not contain) carrying `role="button"`, `tabIndex`, an "Open <title> in webchat" aria-label, and Enter/Space activation, styled with `conv-main-clickable` for pointer + hover parity with the channel-row button. The click and keyboard handlers no-op while the row is being renamed, so an in-progress edit is preserved; the action icons and the `⋯` overflow menu are a sibling cluster running their own handlers, so activating them never triggers the row-body open. At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`, `Re-seat`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. **Re-seat:** Re-seat is a member action of the one `SessionRowActions` cluster, not a separate sibling control — wide it is the sliders icon inline with the other icons (sharing the cluster geometry and alignment); collapsed it is the `Re-seat` menu item, which expands the model/mode/effort form inline inside the overflow menu. The form (`SessionReseatControl`) forks the session via `/api/admin/session-reseat` and navigates to the fork (the same fork mechanism `/chat`'s pickers drive); its model picker defaults to the row's current model (a `model` field on each row, read from the JSONL tail), and mode and effort each carry a "Keep" option that omits the field. The overflow menu and the Re-seat form render through a `document.body` portal, fixed-positioned from the trigger's rect (right-aligned, flipped above the trigger when there is no room below), so neither is clipped by `.side-list`'s `overflow-y:auto`; Escape, a pointerdown outside the trigger/popover, and any scroll or resize dismiss them. | `/claude-sessions/events`, `/claude-sessions`, `/session-reseat` |
369
380
  | Data pane | A "Data" nav row beside Artefacts/Sessions. Uses the same `activeNav` toggle, but its content — the headless `DataFileBrowser` (graph search + `{installDir}/data/` file browser) — renders in the shell's **main column** via `DataPaneSurface`, like the WhatsApp Transcript, not in the side-list; there is no side-list block under `activeNav==='data'`. `AdminShell` owns a `dataOpen` main-column state (mutually exclusive with the WhatsApp selection); the row lifts the choice through `onSelectData`. On `/` it opens in place; from any other route (`/graph`, `/chat`, `/browser`) it navigates to `/?data=1`, which the root shell hydrates on mount (`hydrateDataFromUrl`, then strips the query). The admin burger no longer carries a Data item (the operator surface, which has no Sidebar, keeps the burger → `/data` route). Client breadcrumbs: `[admin-ui] sidebar-nav surface=data …`, `[admin-ui] data-open route=… via=<in-place\|navigate>`, `[admin-ui] data-hydrate route=/`. | `sidebar-artefacts.ts`, `files.ts`, `graph-search.ts` |
370
- | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). Server logs `[wa-reader] op=list accountId=<id> conversations=<n> adminScope=<n> publicScope=<n>` per list build (`conversations=0` on an account with a non-empty store dir is the drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/whatsapp-reader/conversations` |
371
- | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /whatsapp-reader/directives?sessionId=`, bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
381
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row per channel that has ≥1 conversation. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no conversations shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount**, so a failed load shows one muted, non-clickable line instead of channel rows. **WhatsApp rows are sourced from the on-disk message store** (`readAllConversations`), one row per stored conversation keyed by `remoteJid`, independent of whether any session JSONL exists — a blocked non-admin DM or a group-observed thread that never produced a session is listed here. **Telegram rows are session-sourced** (one per admin Telegram session), unchanged. Each conversation row shows a **display name**: for WhatsApp, the store record's `senderName` (else the `senderTelephone`, else the `remoteJid`); for Telegram, the precedence `operatorName ?? senderId ?? title` where `operatorName` is the bound `Person givenName familyName`. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`) from `lastMessageAt`, never "Invalid Date". **Scope colour (WhatsApp):** each WhatsApp row is coloured by its conversation's scope — a thin left accent bar, green for `admin`, orange for `public`, taken from the conversation's most-recent stored record (this replaces the former "Public" text pill). **Re-seat:** only session-sourced rows (Telegram, and webchat in the Sessions panel) carry a `SessionRowActions` Re-seat cluster; a WhatsApp store row has no session, so it carries no Re-seat. A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route it navigates to `/?wa=<remoteJid>&acct=<accountId>`, which the root shell hydrates on mount (then strips the query). **Account-scoped** exactly as the sidebar Sessions list: both row sources (WhatsApp store rows and Telegram/public-webchat session rows) are scoped to the caller's pinned account (`getAccountIdForSession(cacheKey)`), multi-account-gated (`listValidAccounts().length > 1`). On a multi-account install a subaccount view shows only its own conversations; the house account's WhatsApp appears only under the house view; an untagged session sidecar is excluded and counted; an unresolvable scope fails closed to an empty list (logs `op=list-scope-unresolved`). A single-account install filters nothing, byte-identical to before. Server logs `[wa-reader] op=list pinnedAccount=<id8> resolvedAccount=<id8> multiAccount=<bool> conversations=<n> sessionRows=<m> storeRows=<k> adminScope=<n> publicScope=<n> sessionRowsExcludedUntagged=<n>` per list build (`pinnedAccount === resolvedAccount` is the post-fix invariant when `multiAccount=true`; `conversations=0` on an account with a non-empty store dir is the over-filtering drift signal); the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ remoteJid=…`. | `/whatsapp-reader/conversations` |
382
+ | Channel transcript (`<Transcript>`) | **WhatsApp** reads its conversation verbatim from the message store file (`<remoteJid>.jsonl`) over SSE (`/whatsapp-reader/store-stream`): both directions, `dateSent`-ordered, backlog on connect then a live tail so a newly stored message appears without reload — no session JSONL is read. **Telegram** still reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). On the Telegram session stream, `tool-call`/`tool-result` turns render as **collapsed cards** so the human↔agent text is not buried in JSON. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom**; while scrolled up a **jump-to-bottom control** (`.wa-jump`) appears. Both streams send a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tag each frame with a byte-offset `id:`, and resume from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The Telegram session stream also interleaves a collapsed `⚙ directive injected (N B)` row per turn (listed by `GET /whatsapp-reader/directives?sessionId=`, bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`); a WhatsApp store conversation has no session, so it has no directives. A centered day-divider row (`.day-divider`) marks each local calendar-day crossover. This viewer renders **delivered messages only** (both WhatsApp store rows and Telegram session rows, via `forceDeliveredOnly`) with **no per-viewer toggle** — the former "Messages only" pill is retired; the admin `/chat` composer's own messages-only control is unaffected. The panel — wallpaper included — is **bounded to `--chat-measure` and centred** (`.platform > .wa-reader`), so it reads as a column the same width as `/data`, not an edge-to-edge beige surface. The store stream open logs `[wa-reader] op=open remoteJid=<jid> scope=<admin\|public> messages=<n> source=store`. | `/whatsapp-reader/store-stream`, `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
372
383
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
373
384
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal. | `sidebar-artefacts.ts`, `files.ts` |
374
385
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
@@ -1,6 +1,10 @@
1
1
  # Admin Chat Attachments
2
2
 
3
- What you can drag-and-drop into the admin chat window, what happens to each file, and the size caps.
3
+ What you can drag-and-drop or paste into the admin chat window, what happens to each file, and the size caps.
4
+
5
+ ## How files get in
6
+
7
+ You can drag-and-drop a file, pick one with the paperclip, or paste. All three routes stage the file the same way and enforce the same caps. Pasting a file you copied from your file manager (or another app) keeps its real name and type, so `report.pdf` stays `report.pdf`. Pasting a screenshot or a raw clipboard image has no filename to keep, so it is staged under a generated unique name like `pasted-image-1700000000000.png`. Pasting plain text still types the text into the message box as before.
4
8
 
5
9
  ## Accepted file types
6
10
 
@@ -21,7 +21,7 @@ Activate when the operator asks for any of:
21
21
  Do **not** activate for:
22
22
 
23
23
  - Listing or searching emails as the end-goal (no draft intent) — let `email-search` / `email-graph-query` answer directly.
24
- - Setup, status checks, OTP extraction — those are direct tool calls. (For Google/Workspace setup the agent must require an app password, never the regular password — see the email plugin's `references/email-reference.md` § Setup → Google. For Microsoft / Microsoft 365 addresses there is no email-plugin path at all — register via the `outlook` Graph plugin's `outlook-account-register`, not `email-setup` — see § Setup → Microsoft.)
24
+ - Setup, status checks, OTP extraction — those are direct tool calls. (For Google/Workspace setup the agent must require an app password, never the regular password — see the email plugin's `references/email-reference.md` § Setup → Google, opened with the `plugin-read` tool (`file="references/email-reference.md"`), which resolves it at the plugin root; a plain `Read` against this skill's base directory misses. For Microsoft / Microsoft 365 addresses there is no email-plugin path at all — register via the `outlook` Graph plugin's `outlook-account-register`, not `email-setup` — see § Setup → Microsoft.)
25
25
  - Any automated send loop. If the operator asks for drip / scheduled / broadcast sends, decline and explain that this skill is human-in-the-loop only.
26
26
 
27
27
  ## Flow — reply to an existing thread
@@ -63,5 +63,7 @@ Subsequent tool calls use the persisted refresh token to mint access tokens tran
63
63
 
64
64
  ## Reference files
65
65
 
66
+ Open each with the `plugin-read` tool (`file="references/auth.md"`), which resolves the path at the plugin root. A plain `Read` of `references/auth.md` against this skill's base directory misses, because these files live one level up at the plugin root, not under the skill.
67
+
66
68
  - [`references/auth.md`](references/auth.md) — Entra app registration + PKCE flow + re-vendor procedure
67
69
  - [`references/graph-surfaces.md`](references/graph-surfaces.md) — Graph endpoint and response shape per tool
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=d1-command.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"d1-command.test.d.ts","sourceRoot":"","sources":["../../../src/lib/__tests__/d1-command.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ // Regression guard for Task 1350. The brand server PATH carries no global
2
+ // `wrangler`; only `npx wrangler` resolves, the same form the proven
3
+ // forward-path availability publisher uses. A bare-`wrangler` spawn head
4
+ // returns ENOENT on every reconcile tick, so booking->calendar reverse-sync
5
+ // silently fails. This pins the resolved command to the npx form.
6
+ import { describe, it, expect } from "vitest";
7
+ import { buildD1Command } from "../d1-command.js";
8
+ describe("buildD1Command", () => {
9
+ it("invokes wrangler via npx, not a bare wrangler binary", () => {
10
+ const { command, args } = buildD1Command("mydb", "SELECT 1");
11
+ expect(command).toBe("npx");
12
+ expect(args[0]).toBe("wrangler");
13
+ expect(command).not.toBe("wrangler");
14
+ });
15
+ it("preserves the d1 execute argv shape and the SQL/db it was given", () => {
16
+ const { args } = buildD1Command("mydb", "SELECT 1");
17
+ expect(args).toEqual([
18
+ "wrangler", "d1", "execute", "mydb",
19
+ "--remote", "--json", "--command", "SELECT 1",
20
+ ]);
21
+ });
22
+ });
23
+ //# sourceMappingURL=d1-command.test.js.map