@rubytech/create-maxy-code 0.1.305 → 0.1.307

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 (179) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/models/dist/index.d.ts +0 -1
  3. package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/models/dist/index.js +2 -5
  5. package/payload/platform/lib/models/dist/index.js.map +1 -1
  6. package/payload/platform/lib/models/src/index.ts +1 -4
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +46 -12
  8. package/payload/platform/plugins/docs/references/admin-ui.md +32 -11
  9. package/payload/platform/plugins/docs/references/getting-started.md +13 -0
  10. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  11. package/payload/platform/services/claude-session-manager/dist/http-server.js +14 -0
  12. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  13. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  14. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +8 -0
  15. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +13 -0
  17. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +18 -0
  19. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  20. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  21. package/payload/platform/services/whatsapp-channel/dist/server.js +34 -10
  22. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  23. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +9 -0
  24. package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
  25. package/payload/platform/services/whatsapp-channel/dist/targets.js +13 -0
  26. package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
  27. package/payload/platform/templates/account.json +1 -1
  28. package/payload/server/{chunk-TUY3RB5G.js → chunk-4QFPGAUZ.js} +27 -0
  29. package/payload/server/maxy-edge.js +1 -1
  30. package/payload/server/public/assets/AdminLoginScreens-BsC-EwAn.js +1 -0
  31. package/payload/server/public/assets/AdminShell-CiKKN22G.js +1 -0
  32. package/payload/server/public/assets/Checkbox-DoPOlZTV.js +1 -0
  33. package/payload/server/public/assets/admin-DSwmdCKp.js +1 -0
  34. package/payload/server/public/assets/{arc-BrTRjBIM.js → arc-DxSm8tli.js} +1 -1
  35. package/payload/server/public/assets/architecture-YZFGNWBL-6g9jRVgc.js +1 -0
  36. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DJ9on7sZ.js → architectureDiagram-Q4EWVU46-7JSAh0vL.js} +1 -1
  37. package/payload/server/public/assets/{audio-attachment-mime-j2VvxEtx.js → audio-attachment-mime-BA10IzqQ.js} +4 -4
  38. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-CIcyQn0q.js → blockDiagram-DXYQGD6D-DLoVmHKD.js} +1 -1
  39. package/payload/server/public/assets/brand-BaRMO3mZ.js +9 -0
  40. package/payload/server/public/assets/{jsx-runtime-DZkld3Wk.css → brand-C001cin3.css} +1 -1
  41. package/payload/server/public/assets/browser-BKR1eJJb.js +1 -0
  42. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Dt5dh_GR.js → c4Diagram-AHTNJAMY-Nv1G0o9P.js} +1 -1
  43. package/payload/server/public/assets/channel-BgQLJanG.js +1 -0
  44. package/payload/server/public/assets/chat-DZXNRiy-.js +1 -0
  45. package/payload/server/public/assets/{chunk-2KRD3SAO-BUq3MqUy.js → chunk-2KRD3SAO-BpqPMhoA.js} +1 -1
  46. package/payload/server/public/assets/{chunk-336JU56O-CUzJH5Qt.js → chunk-336JU56O-DgXHlVU_.js} +2 -2
  47. package/payload/server/public/assets/chunk-426QAEUC-Dr_XVuUq.js +1 -0
  48. package/payload/server/public/assets/{chunk-4BX2VUAB-B4JUc3UA.js → chunk-4BX2VUAB-nwyZSZH8.js} +1 -1
  49. package/payload/server/public/assets/{chunk-4TB4RGXK-HRO9fIIs.js → chunk-4TB4RGXK-Tfxz9LHX.js} +1 -1
  50. package/payload/server/public/assets/{chunk-55IACEB6-MZdrLBuw.js → chunk-55IACEB6-6FMJQGXG.js} +1 -1
  51. package/payload/server/public/assets/{chunk-5FUZZQ4R-XcAN5zlM.js → chunk-5FUZZQ4R-g2FgZF3D.js} +1 -1
  52. package/payload/server/public/assets/{chunk-5PVQY5BW-PQ1GEsxo.js → chunk-5PVQY5BW-DtLwXsPH.js} +1 -1
  53. package/payload/server/public/assets/{chunk-67CJDMHE-BVvCaqGd.js → chunk-67CJDMHE-K91CP5ZU.js} +1 -1
  54. package/payload/server/public/assets/{chunk-7N4EOEYR-BlBbZdpM.js → chunk-7N4EOEYR-BGpCQhqK.js} +1 -1
  55. package/payload/server/public/assets/{chunk-AA7GKIK3-Ct-PgkGa.js → chunk-AA7GKIK3-jkDbInck.js} +1 -1
  56. package/payload/server/public/assets/{chunk-BSJP7CBP-DZRRfCSz.js → chunk-BSJP7CBP-mDosdzGs.js} +1 -1
  57. package/payload/server/public/assets/{chunk-CIAEETIT-BoGEJiXe.js → chunk-CIAEETIT-DhBxewpQ.js} +1 -1
  58. package/payload/server/public/assets/{chunk-EDXVE4YY-DiSmN31f.js → chunk-EDXVE4YY-B7c-9Emg.js} +1 -1
  59. package/payload/server/public/assets/{chunk-ENJZ2VHE-uhhokIvs.js → chunk-ENJZ2VHE-Cqhhncox.js} +1 -1
  60. package/payload/server/public/assets/{chunk-FMBD7UC4-CfCx5WI7.js → chunk-FMBD7UC4-BpDq6wj1.js} +1 -1
  61. package/payload/server/public/assets/{chunk-FOC6F5B3-Dj9E9gTu.js → chunk-FOC6F5B3-Ds02-ktu.js} +1 -1
  62. package/payload/server/public/assets/{chunk-ICPOFSXX-DcBs_FNy.js → chunk-ICPOFSXX-BTX5POFr.js} +2 -2
  63. package/payload/server/public/assets/{chunk-K5T4RW27-7-VefyJD.js → chunk-K5T4RW27-Du1PXXBU.js} +1 -1
  64. package/payload/server/public/assets/{chunk-KGLVRYIC-CqgTEVxm.js → chunk-KGLVRYIC-siasOAf8.js} +1 -1
  65. package/payload/server/public/assets/{chunk-LIHQZDEY-C0gzDXha.js → chunk-LIHQZDEY-xf1rbZtf.js} +1 -1
  66. package/payload/server/public/assets/{chunk-ORNJ4GCN-JXz23R1H.js → chunk-ORNJ4GCN-DeTLQ_LD.js} +1 -1
  67. package/payload/server/public/assets/{chunk-OYMX7WX6-C2snCNq9.js → chunk-OYMX7WX6-DQ7_oVJn.js} +1 -1
  68. package/payload/server/public/assets/chunk-QZHKN3VN-DF4o9HRJ.js +1 -0
  69. package/payload/server/public/assets/{chunk-U2HBQHQK-CXCU7M-M.js → chunk-U2HBQHQK-CXmFUKgn.js} +1 -1
  70. package/payload/server/public/assets/{chunk-X2U36JSP-CEdcSb28.js → chunk-X2U36JSP-Bj8-8Wkz.js} +1 -1
  71. package/payload/server/public/assets/{chunk-XPW4576I-Cckc75YS.js → chunk-XPW4576I-Con3TXqt.js} +1 -1
  72. package/payload/server/public/assets/{chunk-YZCP3GAM-4nN8AO7H.js → chunk-YZCP3GAM-Dx8BHGWf.js} +1 -1
  73. package/payload/server/public/assets/{chunk-ZZ45TVLE-DcoBc8-7.js → chunk-ZZ45TVLE-Dp4Q5Y-f.js} +1 -1
  74. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DAbPKU6u.js +1 -0
  75. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BKuFQLcv.js +1 -0
  76. package/payload/server/public/assets/clone-DnNHGhNe.js +1 -0
  77. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-BFFro_t9.js → cose-bilkent-S5V4N54A-pZiCv69E.js} +1 -1
  78. package/payload/server/public/assets/{dagre-CN5to_JM.js → dagre-CLqhEgbL.js} +1 -1
  79. package/payload/server/public/assets/{dagre-KV5264BT-WYrPwDg0.js → dagre-KV5264BT-BCxE8iyk.js} +1 -1
  80. package/payload/server/public/assets/data-CuVmW1WN.js +1 -0
  81. package/payload/server/public/assets/{diagram-5BDNPKRD-Dz2CiboN.js → diagram-5BDNPKRD-slUEdZzz.js} +1 -1
  82. package/payload/server/public/assets/{diagram-G4DWMVQ6-r8h3Ip9O.js → diagram-G4DWMVQ6-DPcraMfu.js} +1 -1
  83. package/payload/server/public/assets/{diagram-MMDJMWI5-CECcpGtr.js → diagram-MMDJMWI5-DR6luhGK.js} +1 -1
  84. package/payload/server/public/assets/{diagram-TYMM5635-CHOcAyIP.js → diagram-TYMM5635-Jap1B4wA.js} +1 -1
  85. package/payload/server/public/assets/{erDiagram-SMLLAGMA-YIqKdPbS.js → erDiagram-SMLLAGMA-DJKUs2hO.js} +1 -1
  86. package/payload/server/public/assets/file-download-4_hQmMwU.js +1 -0
  87. package/payload/server/public/assets/{flatten-DhYYI_lo.js → flatten-Cl1Bc3dR.js} +1 -1
  88. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DaEvQWpj.js → flowDiagram-DWJPFMVM-C8W-ZZ9W.js} +1 -1
  89. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-x1hClEOB.js → ganttDiagram-T4ZO3ILL-DYCX4Xu2.js} +1 -1
  90. package/payload/server/public/assets/gitGraph-7Q5UKJZL-g7mvu6IY.js +1 -0
  91. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-C-sZsh6Z.js → gitGraphDiagram-UUTBAWPF-CZu_fpgS.js} +1 -1
  92. package/payload/server/public/assets/{graph-Uaa5heM5.js → graph-D29p1FJH.js} +3 -3
  93. package/payload/server/public/assets/graph-labels-DF3QVHkH.js +1 -0
  94. package/payload/server/public/assets/{graphlib-CNkAqm2w.js → graphlib-Bdp7TA4y.js} +1 -1
  95. package/payload/server/public/assets/info-OMHHGYJF-DZqJuIkB.js +1 -0
  96. package/payload/server/public/assets/infoDiagram-42DDH7IO-cGfGccHz.js +2 -0
  97. package/payload/server/public/assets/{isEmpty-DGhq_DBP.js → isEmpty-D1pGOD1J.js} +1 -1
  98. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-wHE-RwzZ.js → ishikawaDiagram-UXIWVN3A-B7KfIX6z.js} +1 -1
  99. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DQB6iThz.js → journeyDiagram-VCZTEJTY-CSDwBfhF.js} +1 -1
  100. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-Ck08vXSA.js → kanban-definition-6JOO6SKY-CWzcEKTr.js} +1 -1
  101. package/payload/server/public/assets/{line-Cdc2FBEU.js → line-BNQSlbYn.js} +1 -1
  102. package/payload/server/public/assets/{linear-DhtUm5H7.js → linear-BN6kGN93.js} +1 -1
  103. package/payload/server/public/assets/{mermaid-parser.core-CSYulPIL.js → mermaid-parser.core-CcFhkElq.js} +2 -2
  104. package/payload/server/public/assets/{mermaid.core-CP-hOmMh.js → mermaid.core-BsIeJ7Cc.js} +3 -3
  105. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-GxlkvV1Z.js → mindmap-definition-QFDTVHPH-CQVzj6D_.js} +1 -1
  106. package/payload/server/public/assets/operator-CQRaBNN7.js +1 -0
  107. package/payload/server/public/assets/{ordinal-CibZU3PM.js → ordinal-CBZeH4Yp.js} +1 -1
  108. package/payload/server/public/assets/packet-4T2RLAQJ-Hn8rlHpy.js +1 -0
  109. package/payload/server/public/assets/page-Dz2F3QtB.js +1 -0
  110. package/payload/server/public/assets/pie-ZZUOXDRM-BPGWVqBm.js +1 -0
  111. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Di6kWh9c.js → pieDiagram-DEJITSTG-DnrQ9fyn.js} +1 -1
  112. package/payload/server/public/assets/{public-DBXfM02B.js → public-B7BQwWpP.js} +3 -3
  113. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-iNHC5bkO.js → quadrantDiagram-34T5L4WZ-BQWlCyWi.js} +1 -1
  114. package/payload/server/public/assets/radar-PYXPWWZC-DMdLrj3E.js +1 -0
  115. package/payload/server/public/assets/{reduce-Da0RBva0.js → reduce-PuPlFPRX.js} +1 -1
  116. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BBlLREXo.js → requirementDiagram-MS252O5E-Du_vZhU1.js} +1 -1
  117. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-qyDkP-Ca.js → sankeyDiagram-XADWPNL6-Cf6Z8GPQ.js} +1 -1
  118. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-C0MJuS0m.js → sequenceDiagram-FGHM5R23-CS8GVol8.js} +1 -1
  119. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-aHTrlosa.js → stateDiagram-FHFEXIEX-Bv1NW2F1.js} +1 -1
  120. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BGUCxPmp.js +1 -0
  121. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BlRZIxEp.js → timeline-definition-GMOUNBTQ-ClGYAsr0.js} +1 -1
  122. package/payload/server/public/assets/treeView-SZITEDCU-CWxofUbI.js +1 -0
  123. package/payload/server/public/assets/treemap-W4RFUUIX-Cfgb_ZG0.js +1 -0
  124. package/payload/server/public/assets/useSelectionMode-ghXuZicl.js +5 -0
  125. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-s76adysx.js → vennDiagram-DHZGUBPP-C2wTcxQT.js} +1 -1
  126. package/payload/server/public/assets/wardley-RL74JXVD-B45olYjj.js +1 -0
  127. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-xC8t2QGW.js → wardleyDiagram-NUSXRM2D-D86-ivEx.js} +1 -1
  128. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DRcHg-Wj.js → xychartDiagram-5P7HB3ND-Ba5WAN8P.js} +1 -1
  129. package/payload/server/public/browser.html +6 -6
  130. package/payload/server/public/chat.html +9 -10
  131. package/payload/server/public/data.html +5 -5
  132. package/payload/server/public/graph.html +8 -8
  133. package/payload/server/public/index.html +8 -9
  134. package/payload/server/public/operator.html +11 -12
  135. package/payload/server/public/public.html +7 -7
  136. package/payload/server/public/sw.js +24 -0
  137. package/payload/server/server.js +637 -226
  138. package/payload/server/public/assets/AdminLoginScreens-Cw882rmm.js +0 -1
  139. package/payload/server/public/assets/AdminShell-DpWl1uoY.js +0 -1
  140. package/payload/server/public/assets/Checkbox-DAWZItdK.js +0 -1
  141. package/payload/server/public/assets/Transcript-DbK2YA3K.js +0 -1
  142. package/payload/server/public/assets/admin-nQRdn79w.js +0 -1
  143. package/payload/server/public/assets/architecture-YZFGNWBL-DqQB3T6j.js +0 -1
  144. package/payload/server/public/assets/browser-CHWZDCXt.js +0 -1
  145. package/payload/server/public/assets/channel-NZeJdLVK.js +0 -1
  146. package/payload/server/public/assets/chat-BBK6ulFy.js +0 -1
  147. package/payload/server/public/assets/chunk-426QAEUC-DFKX5l0L.js +0 -1
  148. package/payload/server/public/assets/chunk-QZHKN3VN-BWCCN0AM.js +0 -1
  149. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DPlCQCX2.js +0 -1
  150. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CqkQq74h.js +0 -1
  151. package/payload/server/public/assets/clone-CSykdD1A.js +0 -1
  152. package/payload/server/public/assets/data-33mdnHv8.js +0 -1
  153. package/payload/server/public/assets/file-download-P_qlMCg5.js +0 -1
  154. package/payload/server/public/assets/gitGraph-7Q5UKJZL-V7Tx7_eY.js +0 -1
  155. package/payload/server/public/assets/graph-labels-rG63rHvl.js +0 -1
  156. package/payload/server/public/assets/info-OMHHGYJF-D3DqVPGn.js +0 -1
  157. package/payload/server/public/assets/infoDiagram-42DDH7IO-B_O-R0vN.js +0 -2
  158. package/payload/server/public/assets/jsx-runtime-sFVKdwlP.js +0 -9
  159. package/payload/server/public/assets/operator-CiDwt5I9.js +0 -1
  160. package/payload/server/public/assets/packet-4T2RLAQJ-BY7mgCTQ.js +0 -1
  161. package/payload/server/public/assets/page-BvkaS_tu.js +0 -1
  162. package/payload/server/public/assets/pie-ZZUOXDRM-B9f6jN54.js +0 -1
  163. package/payload/server/public/assets/radar-PYXPWWZC-BKS8JOUe.js +0 -1
  164. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-V2vo4BjY.js +0 -1
  165. package/payload/server/public/assets/treeView-SZITEDCU-bIKzyIdw.js +0 -1
  166. package/payload/server/public/assets/treemap-W4RFUUIX-CQycQ4H9.js +0 -1
  167. package/payload/server/public/assets/useSelectionMode-Dky-Stnf.js +0 -5
  168. package/payload/server/public/assets/wardley-RL74JXVD-COr9o-nG.js +0 -1
  169. /package/payload/server/public/assets/{_baseFor-BFYpZGkN.js → _baseFor-brRjpUOX.js} +0 -0
  170. /package/payload/server/public/assets/{array-CYkMkqnU.js → array-BGFCBI0e.js} +0 -0
  171. /package/payload/server/public/assets/{cytoscape.esm-DqlHsaog.js → cytoscape.esm-TvRJ2tGX.js} +0 -0
  172. /package/payload/server/public/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CRZydyG6.js} +0 -0
  173. /package/payload/server/public/assets/{dist-t9p-NJH2.js → dist-Dbh7HrZX.js} +0 -0
  174. /package/payload/server/public/assets/{init-Rr1s_RiX.js → init-B8gtcn7T.js} +0 -0
  175. /package/payload/server/public/assets/{katex-pVJiI_rc.js → katex-RxgSu_dy.js} +0 -0
  176. /package/payload/server/public/assets/{path-BAQ3hXlG.js → path-DZF-JdEe.js} +0 -0
  177. /package/payload/server/public/assets/{preload-helper-B_l5jfgx.js → preload-helper-CQ36nxok.js} +0 -0
  178. /package/payload/server/public/assets/{rough.esm-nHaDi0Kw.js → rough.esm-6CnTHTkH.js} +0 -0
  179. /package/payload/server/public/assets/{src-DuPQVw-H.js → src-BoaldmnP.js} +0 -0
@@ -69,6 +69,7 @@ import {
69
69
  listAdminSessions,
70
70
  listValidAccounts,
71
71
  loadAdminUserName,
72
+ loadPersonNameByElementId,
72
73
  loadUserProfile,
73
74
  loadWhatsappSenderName,
74
75
  persistMessage,
@@ -105,7 +106,7 @@ import {
105
106
  vncLog,
106
107
  walkPremiumBundles,
107
108
  writeAdminUserAndPerson
108
- } from "./chunk-TUY3RB5G.js";
109
+ } from "./chunk-4QFPGAUZ.js";
109
110
  import {
110
111
  __commonJS,
111
112
  __toESM
@@ -1223,10 +1224,93 @@ var serveStatic = (options = { root: "" }) => {
1223
1224
 
1224
1225
  // server/index.ts
1225
1226
  import { readFileSync as readFileSync26, existsSync as existsSync27, watchFile } from "fs";
1226
- import { resolve as resolve29, join as join22, basename as basename8 } from "path";
1227
+ import { resolve as resolve30, join as join22, basename as basename8 } from "path";
1227
1228
  import { homedir as homedir3 } from "os";
1228
1229
  import { monitorEventLoopDelay } from "perf_hooks";
1229
1230
 
1231
+ // app/lib/pwa.ts
1232
+ var PWA_SURFACES = [
1233
+ {
1234
+ surface: "admin",
1235
+ shellFile: "index.html",
1236
+ manifestPath: "/manifest-admin.webmanifest",
1237
+ nameSuffix: "",
1238
+ startUrl: "/",
1239
+ scope: "/",
1240
+ id: "/?pwa=admin"
1241
+ },
1242
+ {
1243
+ surface: "operator",
1244
+ shellFile: "operator.html",
1245
+ manifestPath: "/manifest-operator.webmanifest",
1246
+ nameSuffix: " Operator",
1247
+ startUrl: "/",
1248
+ scope: "/",
1249
+ id: "/?pwa=operator"
1250
+ },
1251
+ {
1252
+ surface: "chat",
1253
+ shellFile: "chat.html",
1254
+ manifestPath: "/manifest-chat.webmanifest",
1255
+ nameSuffix: " Chat",
1256
+ startUrl: "/chat",
1257
+ scope: "/chat",
1258
+ id: "/chat?pwa=chat"
1259
+ }
1260
+ ];
1261
+ var MANIFEST_CONTENT_TYPE = "application/manifest+json";
1262
+ var SW_CONTENT_TYPE = "application/javascript";
1263
+ function pwaSurfaceForShell(file) {
1264
+ return PWA_SURFACES.find((s) => s.shellFile === file);
1265
+ }
1266
+ function buildManifest(s, b) {
1267
+ const name = `${b.productName}${s.nameSuffix}`;
1268
+ return {
1269
+ id: s.id,
1270
+ name,
1271
+ short_name: name,
1272
+ start_url: s.startUrl,
1273
+ scope: s.scope,
1274
+ display: "standalone",
1275
+ theme_color: b.themeColor,
1276
+ background_color: b.backgroundColor,
1277
+ icons: [{ src: b.iconPath, sizes: "any", purpose: "any" }]
1278
+ };
1279
+ }
1280
+ function escAttr(s) {
1281
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1282
+ }
1283
+ function swRegisterScript(s) {
1284
+ return [
1285
+ "<script>",
1286
+ "if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(e){",
1287
+ `try{fetch('/api/_client-error',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({kind:'event',source:'pwa',op:'sw-register-fail',surface:'${s.surface}',scope:'${s.scope}',err:String((e&&e.message)||e)}),keepalive:true}).catch(function(){});}catch(_){}`,
1288
+ "});}",
1289
+ "</script>"
1290
+ ].join("\n");
1291
+ }
1292
+ function pwaHeadTags(s, b) {
1293
+ return [
1294
+ `<link rel="manifest" href="${escAttr(s.manifestPath)}">`,
1295
+ `<link rel="apple-touch-icon" href="${escAttr(b.iconPath)}">`,
1296
+ // Both the standards-based and the legacy Apple capable meta: Chromium
1297
+ // deprecates `apple-mobile-web-app-capable` in favour of
1298
+ // `mobile-web-app-capable`, while iOS Safari still reads the apple one.
1299
+ '<meta name="mobile-web-app-capable" content="yes">',
1300
+ '<meta name="apple-mobile-web-app-capable" content="yes">',
1301
+ `<meta name="theme-color" content="${escAttr(b.themeColor)}">`,
1302
+ swRegisterScript(s)
1303
+ ].join("\n");
1304
+ }
1305
+ function pwaCensus(d) {
1306
+ const manifestLinked = PWA_SURFACES.filter((s) => {
1307
+ const html = d.shellHtml(s.shellFile);
1308
+ return html != null && html.includes(`"${s.manifestPath}"`);
1309
+ }).length;
1310
+ const iconsPresent = d.iconExists() ? PWA_SURFACES.length : 0;
1311
+ return { surfaces: PWA_SURFACES.length, manifestLinked, iconsPresent };
1312
+ }
1313
+
1230
1314
  // server/webchat-descriptor.ts
1231
1315
  import { resolve, join as join2 } from "path";
1232
1316
  function webchatGatewayUrl() {
@@ -1788,7 +1872,7 @@ var credsSaveQueue = Promise.resolve();
1788
1872
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
1789
1873
  console.error(`${TAG2} draining credential save queue\u2026`);
1790
1874
  const timer2 = new Promise(
1791
- (resolve30) => setTimeout(() => resolve30("timeout"), timeoutMs)
1875
+ (resolve31) => setTimeout(() => resolve31("timeout"), timeoutMs)
1792
1876
  );
1793
1877
  const result = await Promise.race([
1794
1878
  credsSaveQueue.then(() => "drained"),
@@ -1916,11 +2000,11 @@ async function createWaSocket(opts) {
1916
2000
  return sock;
1917
2001
  }
1918
2002
  async function waitForConnection(sock) {
1919
- return new Promise((resolve30, reject) => {
2003
+ return new Promise((resolve31, reject) => {
1920
2004
  const handler = (update) => {
1921
2005
  if (update.connection === "open") {
1922
2006
  sock.ev.off("connection.update", handler);
1923
- resolve30();
2007
+ resolve31();
1924
2008
  }
1925
2009
  if (update.connection === "close") {
1926
2010
  sock.ev.off("connection.update", handler);
@@ -2034,14 +2118,14 @@ ${inspected}`;
2034
2118
  return inspect2(err, INSPECT_OPTS2);
2035
2119
  }
2036
2120
  function withTimeout(label, promise, timeoutMs) {
2037
- return new Promise((resolve30, reject) => {
2121
+ return new Promise((resolve31, reject) => {
2038
2122
  const timer2 = setTimeout(() => {
2039
2123
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2040
2124
  }, timeoutMs);
2041
2125
  promise.then(
2042
2126
  (value) => {
2043
2127
  clearTimeout(timer2);
2044
- resolve30(value);
2128
+ resolve31(value);
2045
2129
  },
2046
2130
  (err) => {
2047
2131
  clearTimeout(timer2);
@@ -2925,8 +3009,8 @@ async function persistWhatsAppMessage(input) {
2925
3009
  const { givenName, familyName } = splitName(input.pushName);
2926
3010
  const prev = sessionWriteLocks.get(input.cacheKey);
2927
3011
  let release;
2928
- const mine = new Promise((resolve30) => {
2929
- release = resolve30;
3012
+ const mine = new Promise((resolve31) => {
3013
+ release = resolve31;
2930
3014
  });
2931
3015
  const chained = (prev ?? Promise.resolve()).then(() => mine);
2932
3016
  sessionWriteLocks.set(input.cacheKey, chained);
@@ -4006,11 +4090,11 @@ async function connectWithReconnect(conn) {
4006
4090
  console.error(
4007
4091
  `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4008
4092
  );
4009
- await new Promise((resolve30) => {
4010
- const timer2 = setTimeout(resolve30, delay);
4093
+ await new Promise((resolve31) => {
4094
+ const timer2 = setTimeout(resolve31, delay);
4011
4095
  conn.abortController.signal.addEventListener("abort", () => {
4012
4096
  clearTimeout(timer2);
4013
- resolve30();
4097
+ resolve31();
4014
4098
  }, { once: true });
4015
4099
  });
4016
4100
  }
@@ -4018,16 +4102,16 @@ async function connectWithReconnect(conn) {
4018
4102
  }
4019
4103
  }
4020
4104
  function waitForDisconnectEvent(conn) {
4021
- return new Promise((resolve30) => {
4105
+ return new Promise((resolve31) => {
4022
4106
  if (!conn.sock) {
4023
- resolve30();
4107
+ resolve31();
4024
4108
  return;
4025
4109
  }
4026
4110
  const sock = conn.sock;
4027
4111
  const handler = (update) => {
4028
4112
  if (update.connection === "close") {
4029
4113
  sock.ev.off("connection.update", handler);
4030
- resolve30();
4114
+ resolve31();
4031
4115
  }
4032
4116
  };
4033
4117
  sock.ev.on("connection.update", handler);
@@ -4287,8 +4371,8 @@ async function handleInboundMessage(conn, msg) {
4287
4371
  const conversationKey = isGroup ? remoteJid : senderPhone;
4288
4372
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4289
4373
  let resolvePending;
4290
- const sttPending = new Promise((resolve30) => {
4291
- resolvePending = resolve30;
4374
+ const sttPending = new Promise((resolve31) => {
4375
+ resolvePending = resolve31;
4292
4376
  });
4293
4377
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4294
4378
  try {
@@ -4682,20 +4766,20 @@ function buildX11Env(chromiumWrapperPath, transport = "vnc") {
4682
4766
 
4683
4767
  // server/routes/health.ts
4684
4768
  function checkPort(port2, timeoutMs = 500) {
4685
- return new Promise((resolve30) => {
4769
+ return new Promise((resolve31) => {
4686
4770
  const socket = createConnection2(port2, "127.0.0.1");
4687
4771
  socket.setTimeout(timeoutMs);
4688
4772
  socket.once("connect", () => {
4689
4773
  socket.destroy();
4690
- resolve30(true);
4774
+ resolve31(true);
4691
4775
  });
4692
4776
  socket.once("error", () => {
4693
4777
  socket.destroy();
4694
- resolve30(false);
4778
+ resolve31(false);
4695
4779
  });
4696
4780
  socket.once("timeout", () => {
4697
4781
  socket.destroy();
4698
- resolve30(false);
4782
+ resolve31(false);
4699
4783
  });
4700
4784
  });
4701
4785
  }
@@ -4764,10 +4848,75 @@ var health_default = app;
4764
4848
  // app/lib/attachments.ts
4765
4849
  import { randomUUID as randomUUID4 } from "crypto";
4766
4850
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
4851
+ import { realpathSync as realpathSync2 } from "fs";
4852
+ import { resolve as resolve6, extname } from "path";
4853
+
4854
+ // app/lib/data-path.ts
4767
4855
  import { realpathSync } from "fs";
4768
- import { resolve as resolve5, extname } from "path";
4856
+ import { resolve as resolve5, normalize, sep as sep2, relative } from "path";
4769
4857
  var PLATFORM_ROOT4 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "../platform");
4770
- var ATTACHMENTS_ROOT = resolve5(PLATFORM_ROOT4, "..", "data/uploads");
4858
+ var DATA_ROOT = resolve5(PLATFORM_ROOT4, "..", "data");
4859
+ var CLAUDE_UPLOADS_ROOT = process.env.CLAUDE_CONFIG_DIR ? resolve5(process.env.CLAUDE_CONFIG_DIR, "uploads") : null;
4860
+ var ACCOUNT_PARTITION_DIRS = /* @__PURE__ */ new Set(["accounts"]);
4861
+ var ACCOUNT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4862
+ function crossesForeignAccountPartition(relPath, accountId) {
4863
+ const segs = relPath.split("/").filter(Boolean);
4864
+ if (segs.length < 2) return false;
4865
+ if (!ACCOUNT_PARTITION_DIRS.has(segs[0])) return false;
4866
+ const owner = segs[1];
4867
+ if (!ACCOUNT_UUID_RE.test(owner)) return false;
4868
+ return owner !== accountId;
4869
+ }
4870
+ function resolveUnderRoot(raw, rootAbs, rootLabel) {
4871
+ const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
4872
+ const absolute = resolve5(rootAbs, cleaned);
4873
+ let rootReal;
4874
+ try {
4875
+ rootReal = realpathSync(rootAbs);
4876
+ } catch (err) {
4877
+ return {
4878
+ ok: false,
4879
+ status: 500,
4880
+ error: `${rootLabel} not accessible: ${err instanceof Error ? err.message : String(err)}`
4881
+ };
4882
+ }
4883
+ let resolvedReal;
4884
+ try {
4885
+ resolvedReal = realpathSync(absolute);
4886
+ } catch (err) {
4887
+ const code = err.code;
4888
+ if (code === "ENOENT") {
4889
+ if (absolute !== rootReal && !absolute.startsWith(rootReal + sep2)) {
4890
+ return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: absolute };
4891
+ }
4892
+ return { ok: false, status: 404, error: "Not found" };
4893
+ }
4894
+ return {
4895
+ ok: false,
4896
+ status: 500,
4897
+ error: err instanceof Error ? err.message : String(err)
4898
+ };
4899
+ }
4900
+ if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + sep2)) {
4901
+ return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: resolvedReal };
4902
+ }
4903
+ const relPath = relative(rootReal, resolvedReal) || ".";
4904
+ return { ok: true, absolute: resolvedReal, dataRootReal: rootReal, relative: relPath };
4905
+ }
4906
+ function resolveDataPath(raw) {
4907
+ return resolveUnderRoot(raw, DATA_ROOT, "DATA_ROOT");
4908
+ }
4909
+ function resolveClaudeUploadsPath(raw) {
4910
+ if (!CLAUDE_UPLOADS_ROOT) {
4911
+ return { ok: false, status: 404, error: "Not found" };
4912
+ }
4913
+ return resolveUnderRoot(raw, CLAUDE_UPLOADS_ROOT, "CLAUDE_UPLOADS_ROOT");
4914
+ }
4915
+
4916
+ // app/lib/attachments.ts
4917
+ function uploadsDirFor(accountId, subPath, attachmentId) {
4918
+ return subPath ? resolve6(DATA_ROOT, "accounts", accountId, "uploads", subPath, attachmentId) : resolve6(DATA_ROOT, "accounts", accountId, "uploads", attachmentId);
4919
+ }
4771
4920
  var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
4772
4921
  "image/jpeg",
4773
4922
  "image/png",
@@ -4792,16 +4941,16 @@ function assertSupportedMime(mimeType) {
4792
4941
  );
4793
4942
  }
4794
4943
  }
4795
- async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
4944
+ async function writeAttachment(accountId, subPath, filename, mimeType, sizeBytes, buffer) {
4796
4945
  const attachmentId = randomUUID4();
4797
- const dir = resolve5(ATTACHMENTS_ROOT, scope, attachmentId);
4946
+ const dir = uploadsDirFor(accountId, subPath, attachmentId);
4798
4947
  await mkdir2(dir, { recursive: true });
4799
4948
  const ext = extname(filename) || "";
4800
- const storagePath = resolve5(dir, `${attachmentId}${ext}`);
4801
- const metaPath = resolve5(dir, `${attachmentId}.meta.json`);
4949
+ const storagePath = resolve6(dir, `${attachmentId}${ext}`);
4950
+ const metaPath = resolve6(dir, `${attachmentId}.meta.json`);
4802
4951
  const meta = {
4803
4952
  attachmentId,
4804
- scope,
4953
+ scope: subPath || accountId,
4805
4954
  filename,
4806
4955
  mimeType,
4807
4956
  sizeBytes,
@@ -4819,16 +4968,16 @@ async function storeAttachment(accountId, file) {
4819
4968
  );
4820
4969
  }
4821
4970
  const buffer = Buffer.from(await file.arrayBuffer());
4822
- return writeAttachment(accountId, file.name, file.type, file.size, buffer);
4971
+ return writeAttachment(accountId, "", file.name, file.type, file.size, buffer);
4823
4972
  }
4824
- async function storePublicAttachment(sessionId, file, buffer) {
4973
+ async function storePublicAttachment(accountId, sessionId, file, buffer) {
4825
4974
  assertSupportedMime(file.type);
4826
4975
  if (file.size > MAX_FILE_SIZE_BYTES) {
4827
4976
  throw new Error(
4828
4977
  `File "${file.name}" exceeds the 50 MB limit (${(file.size / 1024 / 1024).toFixed(1)} MB).`
4829
4978
  );
4830
4979
  }
4831
- return writeAttachment(`public/${sessionId}`, file.name, file.type, file.size, buffer);
4980
+ return writeAttachment(accountId, `public/${sessionId}`, file.name, file.type, file.size, buffer);
4832
4981
  }
4833
4982
  var MIME_BY_EXT = {
4834
4983
  ".pdf": "application/pdf",
@@ -4858,8 +5007,8 @@ function detectMimeType(filePath) {
4858
5007
  return MIME_BY_EXT[ext] ?? "application/octet-stream";
4859
5008
  }
4860
5009
  function validateFilePathInAccount(filePath, accountDir) {
4861
- const resolved = realpathSync(filePath);
4862
- const accountResolved = realpathSync(accountDir);
5010
+ const resolved = realpathSync2(filePath);
5011
+ const accountResolved = realpathSync2(accountDir);
4863
5012
  if (!resolved.startsWith(accountResolved + "/")) {
4864
5013
  throw new Error(`File path is outside the account directory`);
4865
5014
  }
@@ -4872,11 +5021,11 @@ async function storeComponentArtefact(accountId, attachmentId, mimeType, content
4872
5021
  `Component artefact exceeds the 50 MB limit (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB).`
4873
5022
  );
4874
5023
  }
4875
- const dir = resolve5(ATTACHMENTS_ROOT, accountId, attachmentId);
5024
+ const dir = uploadsDirFor(accountId, "", attachmentId);
4876
5025
  await mkdir2(dir, { recursive: true });
4877
5026
  const ext = extname(filename) || (mimeType === "text/html" ? ".html" : ".md");
4878
- const storagePath = resolve5(dir, `${attachmentId}${ext}`);
4879
- const metaPath = resolve5(dir, `${attachmentId}.meta.json`);
5027
+ const storagePath = resolve6(dir, `${attachmentId}${ext}`);
5028
+ const metaPath = resolve6(dir, `${attachmentId}.meta.json`);
4880
5029
  const meta = {
4881
5030
  attachmentId,
4882
5031
  scope: accountId,
@@ -4980,7 +5129,7 @@ async function transcribeVoiceNote(file, source) {
4980
5129
  }
4981
5130
 
4982
5131
  // app/lib/channel-pty-bridge/bridge.ts
4983
- import { resolve as resolve7 } from "path";
5132
+ import { resolve as resolve8 } from "path";
4984
5133
 
4985
5134
  // app/lib/channel-pty-bridge/manager-client.ts
4986
5135
  function managerBase() {
@@ -5431,9 +5580,9 @@ async function fanOut(subscribers, text, onError, tag) {
5431
5580
  }
5432
5581
 
5433
5582
  // app/lib/whatsapp/outbound/send-document.ts
5434
- import { realpathSync as realpathSync2 } from "fs";
5583
+ import { realpathSync as realpathSync3 } from "fs";
5435
5584
  import { readFile, stat as stat2 } from "fs/promises";
5436
- import { resolve as resolve6, basename } from "path";
5585
+ import { resolve as resolve7, basename } from "path";
5437
5586
  var TAG16 = "[whatsapp:outbound]";
5438
5587
  var WHATSAPP_DOCUMENT_MAX_BYTES = 100 * 1024 * 1024;
5439
5588
  var lastDocumentOutboundAt = /* @__PURE__ */ new Map();
@@ -5464,11 +5613,11 @@ async function sendWhatsAppDocument(input) {
5464
5613
  if (!maxyAccountId || !platformRoot2) {
5465
5614
  return { ok: false, status: 400, error: "Cannot validate file path: missing account or platform context" };
5466
5615
  }
5467
- const accountDir = resolve6(platformRoot2, "..", "data/accounts", maxyAccountId);
5616
+ const accountDir = resolve7(platformRoot2, "..", "data/accounts", maxyAccountId);
5468
5617
  let resolvedPath;
5469
5618
  try {
5470
- resolvedPath = realpathSync2(filePath);
5471
- const accountResolved = realpathSync2(accountDir);
5619
+ resolvedPath = realpathSync3(filePath);
5620
+ const accountResolved = realpathSync3(accountDir);
5472
5621
  if (!resolvedPath.startsWith(accountResolved + "/")) {
5473
5622
  const sanitised = filePath.replace(accountDir, "<account>/");
5474
5623
  console.error(`${TAG16} document REJECTED path=${sanitised} reason=outside_account_directory`);
@@ -5674,7 +5823,7 @@ async function ensureEntry(input) {
5674
5823
  });
5675
5824
  } else {
5676
5825
  console.error(`${tag} route role=${input.role} target=public-spawn senderId=${input.senderId}`);
5677
- const attachmentDir = resolve7(ATTACHMENTS_ROOT, "public", input.senderId);
5826
+ const attachmentDir = resolve8(DATA_ROOT, "accounts", input.accountId, "uploads", "public", input.senderId);
5678
5827
  spawned = await managerSpawn({
5679
5828
  senderId: input.senderId,
5680
5829
  role: input.role,
@@ -5806,12 +5955,12 @@ async function dispatchOnce(input) {
5806
5955
  });
5807
5956
  if (!entry) return { error: "spawn-failed" };
5808
5957
  entry.lastInboundAt = Date.now();
5809
- let resolve30;
5958
+ let resolve31;
5810
5959
  const turnPromise = new Promise((r) => {
5811
- resolve30 = r;
5960
+ resolve31 = r;
5812
5961
  });
5813
5962
  const listener = (text) => {
5814
- resolve30(text);
5963
+ resolve31(text);
5815
5964
  };
5816
5965
  entry.subscribers.add(listener);
5817
5966
  const writeAtMs = Date.now();
@@ -5960,6 +6109,10 @@ var app2 = new Hono();
5960
6109
  app2.post("/", async (c) => {
5961
6110
  console.log(`[chat-route] entered route=public method=POST`);
5962
6111
  const contentType = c.req.header("content-type") ?? "";
6112
+ const account = resolveAccount();
6113
+ if (!account) {
6114
+ return c.json({ error: "No account configured" }, 500);
6115
+ }
5963
6116
  let message;
5964
6117
  let session_key;
5965
6118
  let userTimestamp;
@@ -6020,7 +6173,7 @@ ${result.result.text}` : result.result.text;
6020
6173
  const stored = [];
6021
6174
  for (const file of regularFiles) {
6022
6175
  const buffer = Buffer.from(await file.arrayBuffer());
6023
- const att = await storePublicAttachment(session_key, file, buffer);
6176
+ const att = await storePublicAttachment(account.accountId, session_key, file, buffer);
6024
6177
  stored.push({ name: file.name, path: att.storagePath, mime: att.mimeType, sizeBytes: att.sizeBytes });
6025
6178
  }
6026
6179
  attachmentCount = stored.length;
@@ -6066,10 +6219,6 @@ ${result.result.text}` : result.result.text;
6066
6219
  );
6067
6220
  const fullMessage = message + uploadNote;
6068
6221
  const encoder = new TextEncoder();
6069
- const account = resolveAccount();
6070
- if (!account) {
6071
- return c.json({ error: "No account configured" }, 500);
6072
- }
6073
6222
  let agentSlug;
6074
6223
  let resolveSource;
6075
6224
  if (requestedSlug) {
@@ -6181,7 +6330,7 @@ ${result.result.text}` : result.result.text;
6181
6330
  var chat_default = app2;
6182
6331
 
6183
6332
  // server/routes/whatsapp.ts
6184
- import { join as join7, resolve as resolve9 } from "path";
6333
+ import { join as join7, resolve as resolve10 } from "path";
6185
6334
  import { readdirSync as readdirSync3, readFileSync as readFileSync8, existsSync as existsSync5 } from "fs";
6186
6335
 
6187
6336
  // app/lib/whatsapp/login.ts
@@ -6189,10 +6338,10 @@ import { randomUUID as randomUUID7 } from "crypto";
6189
6338
 
6190
6339
  // app/lib/whatsapp/config-persist.ts
6191
6340
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
6192
- import { resolve as resolve8, join as join5 } from "path";
6341
+ import { resolve as resolve9, join as join5 } from "path";
6193
6342
  var TAG18 = "[whatsapp:config]";
6194
6343
  function configPath(accountDir) {
6195
- return resolve8(accountDir, "account.json");
6344
+ return resolve9(accountDir, "account.json");
6196
6345
  }
6197
6346
  function readConfig(accountDir) {
6198
6347
  const path2 = configPath(accountDir);
@@ -6599,8 +6748,8 @@ async function startLogin(opts) {
6599
6748
  await clearAuth(authDir);
6600
6749
  let resolveCode = null;
6601
6750
  let rejectCode = null;
6602
- const codePromise = new Promise((resolve30, reject) => {
6603
- resolveCode = resolve30;
6751
+ const codePromise = new Promise((resolve31, reject) => {
6752
+ resolveCode = resolve31;
6604
6753
  rejectCode = reject;
6605
6754
  });
6606
6755
  const codeTimer = setTimeout(
@@ -6997,14 +7146,14 @@ app3.post("/config", async (c) => {
6997
7146
  return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
6998
7147
  }
6999
7148
  case "list-public-agents": {
7000
- const agentsDir = resolve9(account.accountDir, "agents");
7149
+ const agentsDir = resolve10(account.accountDir, "agents");
7001
7150
  const agents = [];
7002
7151
  if (existsSync5(agentsDir)) {
7003
7152
  try {
7004
7153
  const entries = readdirSync3(agentsDir, { withFileTypes: true });
7005
7154
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
7006
7155
  if (!entry.isDirectory() || entry.name === "admin") continue;
7007
- const configPath2 = resolve9(agentsDir, entry.name, "config.json");
7156
+ const configPath2 = resolve10(agentsDir, entry.name, "config.json");
7008
7157
  if (!existsSync5(configPath2)) continue;
7009
7158
  try {
7010
7159
  const config = JSON.parse(readFileSync8(configPath2, "utf-8"));
@@ -7277,15 +7426,17 @@ var whatsapp_default = app3;
7277
7426
 
7278
7427
  // server/routes/whatsapp-reader.ts
7279
7428
  import { readFileSync as readFileSync11, watch, statSync as statSync4, openSync, readSync, closeSync, existsSync as existsSync7, readdirSync as readdirSync6 } from "fs";
7280
- import { basename as basename2, dirname, join as join10, resolve as resolve11, sep as sep2 } from "path";
7429
+ import { basename as basename2, dirname, join as join10, resolve as resolve12, sep as sep3 } from "path";
7281
7430
 
7282
7431
  // server/routes/admin/sidebar-sessions.ts
7283
7432
  import { readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
7284
- import { join as join8, resolve as resolve10 } from "path";
7433
+ import { join as join8, resolve as resolve11 } from "path";
7285
7434
  var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/i;
7286
7435
  var CLI_MARKER_PREFIXES = ["<local-command-", "<command-name>", "<command-message>"];
7287
7436
  var PUBLIC_ROLE = "public";
7437
+ var ADMIN_ROLE = "admin";
7288
7438
  var WEBCHAT_CHANNEL = "webchat";
7439
+ var WHATSAPP_CHANNEL = "whatsapp";
7289
7440
  var app4 = new Hono();
7290
7441
  function claudeConfigDir() {
7291
7442
  return process.env.CLAUDE_CONFIG_DIR ?? null;
@@ -7398,7 +7549,7 @@ function loadUserTitles(accountDir) {
7398
7549
  return out;
7399
7550
  }
7400
7551
  function readSidecarMeta(metaPath) {
7401
- const empty = { bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null };
7552
+ const empty = { bridgeIds: [], role: null, channel: null, senderId: null, startedAt: null, personId: null, adminUserId: null };
7402
7553
  let raw;
7403
7554
  try {
7404
7555
  raw = readFileSync9(metaPath, "utf8");
@@ -7419,7 +7570,9 @@ function readSidecarMeta(metaPath) {
7419
7570
  role: typeof r.role === "string" ? r.role : null,
7420
7571
  channel: typeof r.channel === "string" ? r.channel : null,
7421
7572
  senderId: typeof r.senderId === "string" ? r.senderId : null,
7422
- startedAt: typeof r.startedAt === "string" ? r.startedAt : null
7573
+ startedAt: typeof r.startedAt === "string" ? r.startedAt : null,
7574
+ personId: typeof r.personId === "string" ? r.personId : null,
7575
+ adminUserId: typeof r.adminUserId === "string" ? r.adminUserId : null
7423
7576
  };
7424
7577
  }
7425
7578
  function startsWithCliMarker(s) {
@@ -7429,9 +7582,29 @@ function startsWithCliMarker(s) {
7429
7582
  function trimToTitle(raw) {
7430
7583
  return raw.trim().replace(/\s+/g, " ");
7431
7584
  }
7585
+ var CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
7586
+ var SOURCE_ATTR = /\bsource="([^"]+)"/;
7587
+ var CHANNEL_SOURCE_MAP = {
7588
+ whatsapp: "whatsapp",
7589
+ "whatsapp-channel": "whatsapp",
7590
+ webchat: "webchat",
7591
+ "webchat-channel": "webchat"
7592
+ };
7593
+ function stripChannelEnvelope(text) {
7594
+ const m = text.match(CHANNEL_WRAPPER);
7595
+ if (!m) return { inner: text, channel: null };
7596
+ let raw = m[1].match(SOURCE_ATTR)?.[1] ?? "";
7597
+ let inner = m[2];
7598
+ const im = inner.match(CHANNEL_WRAPPER);
7599
+ const innerRaw = im?.[1].match(SOURCE_ATTR)?.[1];
7600
+ if (im && innerRaw) {
7601
+ raw = innerRaw;
7602
+ inner = im[2];
7603
+ }
7604
+ return { inner, channel: CHANNEL_SOURCE_MAP[raw] ?? (raw || null) };
7605
+ }
7432
7606
  function resolveTitle(body, sessionId, userTitles) {
7433
7607
  const userTitle = userTitles.get(sessionId);
7434
- if (userTitle) return { title: trimToTitle(userTitle), source: "user" };
7435
7608
  let aiTitle = null;
7436
7609
  let firstUserMessage = null;
7437
7610
  for (const line of body.split("\n")) {
@@ -7458,9 +7631,12 @@ function resolveTitle(body, sessionId, userTitles) {
7458
7631
  firstUserMessage = trimmed;
7459
7632
  }
7460
7633
  }
7461
- if (aiTitle) return { title: trimToTitle(aiTitle), source: "ai" };
7462
- if (firstUserMessage) return { title: trimToTitle(firstUserMessage), source: "first-message" };
7463
- return { title: sessionId.slice(0, 8), source: "prefix" };
7634
+ const stripped = firstUserMessage !== null ? stripChannelEnvelope(firstUserMessage) : { inner: null, channel: null };
7635
+ const channel = stripped.channel;
7636
+ if (userTitle) return { title: trimToTitle(userTitle), source: "user", channel };
7637
+ if (aiTitle) return { title: trimToTitle(aiTitle), source: "ai", channel };
7638
+ if (stripped.inner) return { title: trimToTitle(stripped.inner), source: "first-message", channel };
7639
+ return { title: sessionId.slice(0, 8), source: "prefix", channel };
7464
7640
  }
7465
7641
  app4.get("/", requireAdminSession, async (c) => {
7466
7642
  const accountId = process.env.ACCOUNT_ID ?? null;
@@ -7472,7 +7648,7 @@ app4.get("/", requireAdminSession, async (c) => {
7472
7648
  const projectsRoot = join8(configDir2, "projects");
7473
7649
  const jsonls = enumerateJsonls(projectsRoot);
7474
7650
  const liveSessions = await fetchLiveSessions();
7475
- const accountDir = accountId ? resolve10(ACCOUNTS_DIR, accountId) : null;
7651
+ const accountDir = accountId ? resolve11(ACCOUNTS_DIR, accountId) : null;
7476
7652
  const userTitles = loadUserTitles(accountDir);
7477
7653
  const rows = [];
7478
7654
  const seenIds = /* @__PURE__ */ new Set();
@@ -7481,6 +7657,8 @@ app4.get("/", requireAdminSession, async (c) => {
7481
7657
  let nonResumableCount = 0;
7482
7658
  let archivedCount = 0;
7483
7659
  const titleSourceCounts = { user: 0, ai: 0, "first-message": 0, prefix: 0 };
7660
+ const personIdBySession = /* @__PURE__ */ new Map();
7661
+ const adminUserIdBySession = /* @__PURE__ */ new Map();
7484
7662
  for (const { path: path2, isSubagent, archived } of jsonls) {
7485
7663
  const lastSlash = path2.lastIndexOf("/");
7486
7664
  const base = path2.slice(lastSlash + 1);
@@ -7504,8 +7682,12 @@ app4.get("/", requireAdminSession, async (c) => {
7504
7682
  const resolved = resolveTitle(body, sessionId, userTitles);
7505
7683
  titleSourceCounts[resolved.source] += 1;
7506
7684
  const metaPath = join8(projectDir, `${sessionId}.meta.json`);
7507
- const { bridgeIds, role, channel } = readSidecarMeta(metaPath);
7508
- const resumable = !(role === PUBLIC_ROLE && channel === WEBCHAT_CHANNEL);
7685
+ const { bridgeIds, role, channel: sidecarChannel, personId, adminUserId } = readSidecarMeta(metaPath);
7686
+ const resumable = !(role === PUBLIC_ROLE && (sidecarChannel === WEBCHAT_CHANNEL || sidecarChannel === WHATSAPP_CHANNEL));
7687
+ if (personId) personIdBySession.set(sessionId, personId);
7688
+ if (adminUserId && role === ADMIN_ROLE && sidecarChannel === WEBCHAT_CHANNEL) {
7689
+ adminUserIdBySession.set(sessionId, adminUserId);
7690
+ }
7509
7691
  rows.push({
7510
7692
  sessionId,
7511
7693
  title: resolved.title,
@@ -7517,30 +7699,59 @@ app4.get("/", requireAdminSession, async (c) => {
7517
7699
  projectDir,
7518
7700
  bridgeIds,
7519
7701
  resumable,
7520
- archived
7702
+ archived,
7703
+ channel: resolved.channel,
7704
+ personName: null
7521
7705
  });
7522
7706
  if (live) liveCount += 1;
7523
7707
  if (isSubagent) subagentCount += 1;
7524
7708
  if (!resumable) nonResumableCount += 1;
7525
7709
  if (archived) archivedCount += 1;
7526
7710
  }
7711
+ let personNameCount = 0;
7712
+ let adminNameCount = 0;
7713
+ if (accountId) {
7714
+ await Promise.all(
7715
+ rows.map(async (row) => {
7716
+ if (row.titleSource !== "first-message" && row.titleSource !== "prefix") return;
7717
+ const pid = personIdBySession.get(row.sessionId);
7718
+ if (pid) {
7719
+ const name = await loadPersonNameByElementId(accountId, pid);
7720
+ if (name) {
7721
+ row.personName = name;
7722
+ personNameCount += 1;
7723
+ }
7724
+ return;
7725
+ }
7726
+ const auid = adminUserIdBySession.get(row.sessionId);
7727
+ if (auid) {
7728
+ const resolved = await loadAdminUserName(accountId, auid);
7729
+ if (resolved.source === "neo4j") {
7730
+ row.personName = resolved.joined;
7731
+ adminNameCount += 1;
7732
+ }
7733
+ }
7734
+ })
7735
+ );
7736
+ }
7527
7737
  rows.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
7528
7738
  console.log(
7529
- `[admin-sessions-list] rows=${rows.length} live=${liveCount} subagents=${subagentCount} archived=${archivedCount} nonResumable=${nonResumableCount} titles user=${titleSourceCounts.user} ai=${titleSourceCounts.ai} first=${titleSourceCounts["first-message"]} prefix=${titleSourceCounts.prefix} accountId=${accountId ? accountId.slice(0, 8) : "unset"}`
7739
+ `[admin-sessions-list] rows=${rows.length} live=${liveCount} subagents=${subagentCount} archived=${archivedCount} nonResumable=${nonResumableCount} titles user=${titleSourceCounts.user} ai=${titleSourceCounts.ai} first=${titleSourceCounts["first-message"]} prefix=${titleSourceCounts.prefix} personNames=${personNameCount} adminNames=${adminNameCount} accountId=${accountId ? accountId.slice(0, 8) : "unset"}`
7530
7740
  );
7531
7741
  return c.json({ sessions: rows, accountId });
7532
7742
  });
7533
7743
  var sidebar_sessions_default = app4;
7534
7744
 
7535
7745
  // app/lib/whatsapp-reader/select-sessions.ts
7536
- function selectAdminChannelSessions(rows) {
7537
- return rows.filter((r) => r.role === "admin" && (r.channel === "whatsapp" || r.channel === "telegram")).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((r) => ({
7746
+ function selectReaderChannelSessions(rows) {
7747
+ return rows.filter((r) => r.role === "admin" && (r.channel === "whatsapp" || r.channel === "telegram") || r.role === "public" && (r.channel === "whatsapp" || r.channel === "webchat")).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((r) => ({
7538
7748
  sessionId: r.sessionId,
7539
7749
  projectDir: r.projectDir,
7540
7750
  title: r.title,
7541
7751
  senderId: r.senderId,
7542
7752
  startedAt: r.startedAt,
7543
7753
  channel: r.channel,
7754
+ role: r.role,
7544
7755
  operatorName: null,
7545
7756
  whatsappName: null,
7546
7757
  lastMessageAt: r.lastMessageAt
@@ -7644,14 +7855,26 @@ function resolveOperatorDisplay(userIdRes, nameRes) {
7644
7855
  return { name: null, reason: "no-person-name" };
7645
7856
  }
7646
7857
 
7858
+ // app/lib/agent-errors.ts
7859
+ function classifyAgentError(err) {
7860
+ const msg = err instanceof Error ? err.message : String(err);
7861
+ if (/\b(401|authentication_error|OAuth token has expired)\b/i.test(msg)) return "auth_expired";
7862
+ if (/\b(529|overloaded)\b/i.test(msg)) return "claude_overloaded";
7863
+ if (/\b(404|model_not_found)\b/i.test(msg) || /issue with the selected model|may not have access to it/i.test(msg))
7864
+ return "model_unavailable";
7865
+ if (/\b(500|api_error|internal server error)\b/i.test(msg)) return "claude_down";
7866
+ return "agent_error";
7867
+ }
7868
+
7647
7869
  // app/lib/whatsapp-reader/parse-transcript.ts
7648
7870
  var WA_REPLY_TOOL = "mcp__whatsapp-channel__reply";
7649
7871
  var WA_REPLY_DOCUMENT_TOOL = "mcp__whatsapp-channel__reply-document";
7650
7872
  var WEBCHAT_REPLY_TOOL = "mcp__webchat-channel__reply";
7651
7873
  var CHANNEL_REPLY_TOOL = "mcp__channel__reply";
7874
+ var CHANNEL_REPLY_DOCUMENT_TOOL = "mcp__channel__reply-document";
7652
7875
  var CLI_MARKER_PREFIXES2 = ["<local-command-", "<command-name>", "<command-message>"];
7653
- var CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
7654
- var SOURCE_ATTR = /\bsource="([^"]+)"/;
7876
+ var CHANNEL_WRAPPER2 = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
7877
+ var SOURCE_ATTR2 = /\bsource="([^"]+)"/;
7655
7878
  var SOURCE_MAP = {
7656
7879
  whatsapp: "whatsapp",
7657
7880
  "whatsapp-channel": "whatsapp",
@@ -7659,13 +7882,13 @@ var SOURCE_MAP = {
7659
7882
  "webchat-channel": "webchat"
7660
7883
  };
7661
7884
  function unwrapChannel(text) {
7662
- const m = text.match(CHANNEL_WRAPPER);
7885
+ const m = text.match(CHANNEL_WRAPPER2);
7663
7886
  if (!m) return { text };
7664
- let source = SOURCE_MAP[m[1].match(SOURCE_ATTR)?.[1] ?? ""];
7887
+ let source = SOURCE_MAP[m[1].match(SOURCE_ATTR2)?.[1] ?? ""];
7665
7888
  let inner = m[2];
7666
- const im = inner.match(CHANNEL_WRAPPER);
7889
+ const im = inner.match(CHANNEL_WRAPPER2);
7667
7890
  if (im) {
7668
- const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR)?.[1] ?? ""];
7891
+ const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR2)?.[1] ?? ""];
7669
7892
  if (innerSource) {
7670
7893
  source = innerSource;
7671
7894
  inner = im[2];
@@ -7679,6 +7902,17 @@ function asString(content) {
7679
7902
  function tsOf(row) {
7680
7903
  return typeof row.timestamp === "string" ? row.timestamp : null;
7681
7904
  }
7905
+ function joinTextBlocks(content) {
7906
+ if (!Array.isArray(content)) return "";
7907
+ const parts = [];
7908
+ for (const block of content) {
7909
+ if (block && typeof block === "object") {
7910
+ const b = block;
7911
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
7912
+ }
7913
+ }
7914
+ return parts.join("");
7915
+ }
7682
7916
  function assistantTurns(content, ts) {
7683
7917
  if (!Array.isArray(content)) return [];
7684
7918
  const out = [];
@@ -7690,7 +7924,7 @@ function assistantTurns(content, ts) {
7690
7924
  } else if (b.type === "tool_use" && (b.name === WA_REPLY_TOOL || b.name === WEBCHAT_REPLY_TOOL || b.name === CHANNEL_REPLY_TOOL)) {
7691
7925
  const text = b.input?.text;
7692
7926
  out.push({ kind: "agent-reply", text: typeof text === "string" ? text : "", ts });
7693
- } else if (b.type === "tool_use" && b.name === WA_REPLY_DOCUMENT_TOOL) {
7927
+ } else if (b.type === "tool_use" && (b.name === WA_REPLY_DOCUMENT_TOOL || b.name === CHANNEL_REPLY_DOCUMENT_TOOL)) {
7694
7928
  const input = b.input ?? {};
7695
7929
  const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
7696
7930
  const caption = typeof input.caption === "string" ? input.caption : null;
@@ -7736,6 +7970,14 @@ function parseTranscript(lines) {
7736
7970
  const ts = tsOf(row);
7737
7971
  const msg = row.message;
7738
7972
  if (row.type === "assistant") {
7973
+ if (row.isApiErrorMessage === true) {
7974
+ const raw = joinTextBlocks(msg?.content);
7975
+ const status = typeof row.apiErrorStatus === "number" ? String(row.apiErrorStatus) : "";
7976
+ const errorSlug = typeof row.error === "string" ? row.error : "";
7977
+ const code = classifyAgentError(`${status} ${errorSlug} ${raw}`.trim());
7978
+ out.push({ kind: "agent-error", code, raw, ts });
7979
+ continue;
7980
+ }
7739
7981
  out.push(...assistantTurns(msg?.content, ts));
7740
7982
  continue;
7741
7983
  }
@@ -7799,7 +8041,9 @@ app5.get("/conversations", requireAdminSession, async (c) => {
7799
8041
  const sessionId = basename2(path2).replace(/\.jsonl$/, "");
7800
8042
  const projectDir = dirname(path2);
7801
8043
  const meta = readSidecarMeta(join10(projectDir, `${sessionId}.meta.json`));
7802
- if (meta.role !== "admin" || meta.channel !== "whatsapp" && meta.channel !== "telegram") continue;
8044
+ const isAdminChannel = meta.role === "admin" && (meta.channel === "whatsapp" || meta.channel === "telegram");
8045
+ const isPublicChannel = meta.role === "public" && (meta.channel === "whatsapp" || meta.channel === "webchat");
8046
+ if (!isAdminChannel && !isPublicChannel) continue;
7803
8047
  let body = "";
7804
8048
  try {
7805
8049
  body = readFileSync11(path2, "utf8");
@@ -7818,7 +8062,7 @@ app5.get("/conversations", requireAdminSession, async (c) => {
7818
8062
  lastMessageAt
7819
8063
  });
7820
8064
  }
7821
- const selected = selectAdminChannelSessions(rows);
8065
+ const selected = selectReaderChannelSessions(rows);
7822
8066
  const account = resolveAccount();
7823
8067
  let users = [];
7824
8068
  if (account) {
@@ -7885,12 +8129,12 @@ app5.get("/stream", requireAdminSession, (c) => {
7885
8129
  const sessionId = c.req.query("sessionId") ?? "";
7886
8130
  const projectDir = c.req.query("projectDir") ?? "";
7887
8131
  const cfg = claudeConfigDir();
7888
- const projectsRoot = cfg ? resolve11(join10(cfg, "projects")) : null;
8132
+ const projectsRoot = cfg ? resolve12(join10(cfg, "projects")) : null;
7889
8133
  if (!cfg || !projectsRoot || !SESSION_ID_RE2.test(sessionId)) {
7890
8134
  return c.json({ error: "bad session reference" }, 400);
7891
8135
  }
7892
- const jsonlPath = resolve11(join10(projectDir, `${sessionId}.jsonl`));
7893
- if (!jsonlPath.startsWith(projectsRoot + sep2)) {
8136
+ const jsonlPath = resolve12(join10(projectDir, `${sessionId}.jsonl`));
8137
+ if (!jsonlPath.startsWith(projectsRoot + sep3)) {
7894
8138
  return c.json({ error: "bad session reference" }, 400);
7895
8139
  }
7896
8140
  const encoder = new TextEncoder();
@@ -7921,6 +8165,9 @@ data: ${JSON.stringify(turn)}
7921
8165
  let n = 0;
7922
8166
  for (const turn of parseTranscript(lines)) {
7923
8167
  send(controller, turn, offset);
8168
+ if (turn.kind === "agent-error") {
8169
+ console.log(`[wa-stream] op=agent-error conn=${connId} sessionId=${sessionId} code=${turn.code} source=jsonl`);
8170
+ }
7924
8171
  n++;
7925
8172
  }
7926
8173
  if (n > 0) console.log(`[wa-stream] op=drain conn=${connId} turns=${n}`);
@@ -7934,6 +8181,9 @@ data: ${JSON.stringify(turn)}
7934
8181
  let n = 0;
7935
8182
  for (const turn of parseTranscript(buf.toString("utf8").split("\n"))) {
7936
8183
  send(controller, turn, offset);
8184
+ if (turn.kind === "agent-error") {
8185
+ console.log(`[wa-stream] op=agent-error conn=${connId} sessionId=${sessionId} code=${turn.code} source=jsonl`);
8186
+ }
7937
8187
  n++;
7938
8188
  }
7939
8189
  console.log(`[wa-stream] op=backlog conn=${connId} turns=${n} bytes=${end}`);
@@ -7976,7 +8226,7 @@ var DIRECTIVE_NAME_RE = /^\d+-\d+\.txt$/;
7976
8226
  function directiveStoreDir(sessionId) {
7977
8227
  const account = resolveAccount();
7978
8228
  if (!account) return null;
7979
- return resolve11(account.accountDir, "logs", "prompt-optimiser-directives", sessionId);
8229
+ return resolve12(account.accountDir, "logs", "prompt-optimiser-directives", sessionId);
7980
8230
  }
7981
8231
  app5.get("/directives", requireAdminSession, (c) => {
7982
8232
  const sessionId = c.req.query("sessionId") ?? "";
@@ -8010,8 +8260,8 @@ app5.get("/directive", requireAdminSession, (c) => {
8010
8260
  }
8011
8261
  const dir = directiveStoreDir(sessionId);
8012
8262
  if (!dir) return c.json({ error: "no account" }, 404);
8013
- const path2 = resolve11(join10(dir, name));
8014
- if (!path2.startsWith(dir + sep2)) return c.json({ error: "bad reference" }, 400);
8263
+ const path2 = resolve12(join10(dir, name));
8264
+ if (!path2.startsWith(dir + sep3)) return c.json({ error: "bad reference" }, 400);
8015
8265
  let body;
8016
8266
  try {
8017
8267
  body = readFileSync11(path2, "utf8");
@@ -8347,7 +8597,7 @@ ${note}` : note;
8347
8597
  }
8348
8598
 
8349
8599
  // server/routes/webchat-greeting.ts
8350
- import { resolve as resolve12 } from "path";
8600
+ import { resolve as resolve13 } from "path";
8351
8601
 
8352
8602
  // app/lib/claude-agent/specialist-roster.ts
8353
8603
  import { existsSync as existsSync9, readFileSync as readFileSync13, readdirSync as readdirSync8 } from "fs";
@@ -8452,7 +8702,7 @@ app6.get("/", requireAdminSession, async (c) => {
8452
8702
  let specialists = [];
8453
8703
  try {
8454
8704
  const roster = readSpecialistRoster(
8455
- resolve12(ACCOUNTS_DIR, accountId, "specialists", "agents")
8705
+ resolve13(ACCOUNTS_DIR, accountId, "specialists", "agents")
8456
8706
  );
8457
8707
  specialists = roster.specialists;
8458
8708
  for (const s of roster.skipped) {
@@ -8487,7 +8737,7 @@ var webchat_greeting_default = app6;
8487
8737
 
8488
8738
  // server/routes/telegram.ts
8489
8739
  import { homedir } from "os";
8490
- import { resolve as resolve13, join as join13 } from "path";
8740
+ import { resolve as resolve14, join as join13 } from "path";
8491
8741
  import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
8492
8742
 
8493
8743
  // app/lib/telegram/access-control.ts
@@ -8538,7 +8788,7 @@ function routeTelegramUpdate(input) {
8538
8788
  var TAG22 = "[telegram-inbound]";
8539
8789
  var DISPATCH_TIMEOUT_MS = 12e4;
8540
8790
  function configDirName() {
8541
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve13(process.cwd(), "..");
8791
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
8542
8792
  const brandPath = join13(platformRoot2, "config", "brand.json");
8543
8793
  if (existsSync10(brandPath)) {
8544
8794
  try {
@@ -8550,7 +8800,7 @@ function configDirName() {
8550
8800
  }
8551
8801
  function secretPath(botType) {
8552
8802
  const filename = botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret";
8553
- return resolve13(homedir(), configDirName(), filename);
8803
+ return resolve14(homedir(), configDirName(), filename);
8554
8804
  }
8555
8805
  async function sendTelegram(botToken, chatId, text) {
8556
8806
  try {
@@ -9153,7 +9403,7 @@ app9.post("/", async (c) => {
9153
9403
  const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
9154
9404
  return `${k}=${safe}`;
9155
9405
  }).join(" ");
9156
- const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click"]);
9406
+ const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa"]);
9157
9407
  if (TAGGED_PREFIX_SOURCES.has(source)) {
9158
9408
  console.log(
9159
9409
  `[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
@@ -9365,7 +9615,7 @@ var session_default = app10;
9365
9615
 
9366
9616
  // server/routes/admin/logs.ts
9367
9617
  import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync17, statSync as statSync7 } from "fs";
9368
- import { resolve as resolve14, basename as basename4 } from "path";
9618
+ import { resolve as resolve15, basename as basename4 } from "path";
9369
9619
 
9370
9620
  // app/lib/logs-read-resolve.ts
9371
9621
  import { existsSync as existsSync14 } from "fs";
@@ -9392,7 +9642,7 @@ app11.get("/", async (c) => {
9392
9642
  const cacheKeyParam = c.req.query("cacheKey");
9393
9643
  const download = c.req.query("download") === "1";
9394
9644
  const account = resolveAccount();
9395
- const accountLogDir = account ? resolve14(account.accountDir, "logs") : null;
9645
+ const accountLogDir = account ? resolve15(account.accountDir, "logs") : null;
9396
9646
  const logDirs = [];
9397
9647
  if (accountLogDir) logDirs.push(accountLogDir);
9398
9648
  logDirs.push(LOG_DIR);
@@ -9400,7 +9650,7 @@ app11.get("/", async (c) => {
9400
9650
  const safe = basename4(fileParam);
9401
9651
  const searched = [];
9402
9652
  for (const dir of logDirs) {
9403
- const filePath = resolve14(dir, safe);
9653
+ const filePath = resolve15(dir, safe);
9404
9654
  searched.push(filePath);
9405
9655
  try {
9406
9656
  const buffer = readFileSync17(filePath);
@@ -9512,10 +9762,10 @@ app11.get("/", async (c) => {
9512
9762
  console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
9513
9763
  continue;
9514
9764
  }
9515
- files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync7(resolve14(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
9765
+ files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync7(resolve15(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
9516
9766
  seen.add(name);
9517
9767
  try {
9518
- const content = readFileSync17(resolve14(dir, name));
9768
+ const content = readFileSync17(resolve15(dir, name));
9519
9769
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
9520
9770
  logs[name] = tail.trim() || "(empty)";
9521
9771
  } catch (err) {
@@ -9555,7 +9805,7 @@ var claude_info_default = app12;
9555
9805
  // server/routes/admin/attachment.ts
9556
9806
  import { readFile as readFile2, readdir } from "fs/promises";
9557
9807
  import { existsSync as existsSync16 } from "fs";
9558
- import { resolve as resolve15 } from "path";
9808
+ import { resolve as resolve16 } from "path";
9559
9809
  var app13 = new Hono();
9560
9810
  app13.get("/:attachmentId", requireAdminSession, async (c) => {
9561
9811
  const attachmentId = c.req.param("attachmentId");
@@ -9567,11 +9817,11 @@ app13.get("/:attachmentId", requireAdminSession, async (c) => {
9567
9817
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
9568
9818
  return new Response("Not found", { status: 404 });
9569
9819
  }
9570
- const dir = resolve15(ATTACHMENTS_ROOT, accountId, attachmentId);
9820
+ const dir = resolve16(DATA_ROOT, "accounts", accountId, "uploads", attachmentId);
9571
9821
  if (!existsSync16(dir)) {
9572
9822
  return new Response("Not found", { status: 404 });
9573
9823
  }
9574
- const metaPath = resolve15(dir, `${attachmentId}.meta.json`);
9824
+ const metaPath = resolve16(dir, `${attachmentId}.meta.json`);
9575
9825
  if (!existsSync16(metaPath)) {
9576
9826
  return new Response("Not found", { status: 404 });
9577
9827
  }
@@ -9586,7 +9836,7 @@ app13.get("/:attachmentId", requireAdminSession, async (c) => {
9586
9836
  if (!dataFile) {
9587
9837
  return new Response("Not found", { status: 404 });
9588
9838
  }
9589
- const filePath = resolve15(dir, dataFile);
9839
+ const filePath = resolve16(dir, dataFile);
9590
9840
  const buffer = await readFile2(filePath);
9591
9841
  return new Response(new Uint8Array(buffer), {
9592
9842
  headers: {
@@ -9599,13 +9849,13 @@ app13.get("/:attachmentId", requireAdminSession, async (c) => {
9599
9849
  var attachment_default = app13;
9600
9850
 
9601
9851
  // server/routes/admin/agents.ts
9602
- import { resolve as resolve16 } from "path";
9852
+ import { resolve as resolve17 } from "path";
9603
9853
  import { readdirSync as readdirSync11, readFileSync as readFileSync18, existsSync as existsSync17, rmSync } from "fs";
9604
9854
  var app14 = new Hono();
9605
9855
  app14.get("/", (c) => {
9606
9856
  const account = resolveAccount();
9607
9857
  if (!account) return c.json({ agents: [] });
9608
- const agentsDir = resolve16(account.accountDir, "agents");
9858
+ const agentsDir = resolve17(account.accountDir, "agents");
9609
9859
  if (!existsSync17(agentsDir)) return c.json({ agents: [] });
9610
9860
  const agents = [];
9611
9861
  try {
@@ -9613,7 +9863,7 @@ app14.get("/", (c) => {
9613
9863
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
9614
9864
  if (!entry.isDirectory()) continue;
9615
9865
  if (entry.name === "admin") continue;
9616
- const configPath2 = resolve16(agentsDir, entry.name, "config.json");
9866
+ const configPath2 = resolve17(agentsDir, entry.name, "config.json");
9617
9867
  if (!existsSync17(configPath2)) continue;
9618
9868
  try {
9619
9869
  const config = JSON.parse(readFileSync18(configPath2, "utf-8"));
@@ -9642,7 +9892,7 @@ app14.delete("/:slug", async (c) => {
9642
9892
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9643
9893
  return c.json({ error: "Invalid agent slug" }, 400);
9644
9894
  }
9645
- const agentDir = resolve16(account.accountDir, "agents", slug);
9895
+ const agentDir = resolve17(account.accountDir, "agents", slug);
9646
9896
  if (!existsSync17(agentDir)) {
9647
9897
  return c.json({ error: "Agent not found" }, 404);
9648
9898
  }
@@ -9672,7 +9922,7 @@ app14.post("/:slug/project", async (c) => {
9672
9922
  if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
9673
9923
  return c.json({ error: "Invalid agent slug" }, 400);
9674
9924
  }
9675
- const agentDir = resolve16(account.accountDir, "agents", slug);
9925
+ const agentDir = resolve17(account.accountDir, "agents", slug);
9676
9926
  if (!existsSync17(agentDir)) {
9677
9927
  return c.json({ error: "Agent not found on disk" }, 404);
9678
9928
  }
@@ -10376,7 +10626,7 @@ var sessions_default = app15;
10376
10626
 
10377
10627
  // app/lib/claude-agent/spawn-context.ts
10378
10628
  import { existsSync as existsSync19, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync8 } from "fs";
10379
- import { dirname as dirname4, resolve as resolve17, join as join17 } from "path";
10629
+ import { dirname as dirname4, resolve as resolve18, join as join17 } from "path";
10380
10630
  async function resolveOwnerProfileBlock(accountId, userId) {
10381
10631
  if (!userId) return { ok: false, reason: "missing-user-id" };
10382
10632
  try {
@@ -10461,7 +10711,7 @@ function parseSkillPathsFromFrontmatter(fm) {
10461
10711
  }
10462
10712
  function listPluginDirs() {
10463
10713
  const out = /* @__PURE__ */ new Map();
10464
- const platformPlugins = resolve17(PLATFORM_ROOT, "plugins");
10714
+ const platformPlugins = resolve18(PLATFORM_ROOT, "plugins");
10465
10715
  if (existsSync19(platformPlugins)) {
10466
10716
  for (const name of readdirSync12(platformPlugins)) {
10467
10717
  const dir = join17(platformPlugins, name);
@@ -10471,7 +10721,7 @@ function listPluginDirs() {
10471
10721
  }
10472
10722
  }
10473
10723
  }
10474
- const premiumRoot = resolve17(dirname4(PLATFORM_ROOT), "premium-plugins");
10724
+ const premiumRoot = resolve18(dirname4(PLATFORM_ROOT), "premium-plugins");
10475
10725
  if (existsSync19(premiumRoot)) {
10476
10726
  for (const bundle of readdirSync12(premiumRoot)) {
10477
10727
  const bundlePlugins = join17(premiumRoot, bundle, "plugins");
@@ -10491,7 +10741,7 @@ function listPluginDirs() {
10491
10741
  return out;
10492
10742
  }
10493
10743
  function readEnabledPlugins(accountId) {
10494
- const accountFile = resolve17(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
10744
+ const accountFile = resolve18(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
10495
10745
  if (!existsSync19(accountFile)) return /* @__PURE__ */ new Set();
10496
10746
  try {
10497
10747
  const parsed = JSON.parse(readFileSync19(accountFile, "utf-8"));
@@ -10564,7 +10814,7 @@ ${skillFm}
10564
10814
  return out;
10565
10815
  }
10566
10816
  function computeSpecialistDomains(accountId) {
10567
- const specialistsDir = resolve17(PLATFORM_ROOT, "..", "data/accounts", accountId, "specialists", "agents");
10817
+ const specialistsDir = resolve18(PLATFORM_ROOT, "..", "data/accounts", accountId, "specialists", "agents");
10568
10818
  if (!existsSync19(specialistsDir)) return [];
10569
10819
  let entries;
10570
10820
  try {
@@ -10610,7 +10860,7 @@ ${fm}
10610
10860
  return out;
10611
10861
  }
10612
10862
  function computeDormantPlugins(accountId) {
10613
- const accountFile = resolve17(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
10863
+ const accountFile = resolve18(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
10614
10864
  if (!existsSync19(accountFile)) return [];
10615
10865
  let enabled;
10616
10866
  try {
@@ -10625,7 +10875,7 @@ function computeDormantPlugins(accountId) {
10625
10875
  );
10626
10876
  return [];
10627
10877
  }
10628
- const pluginsDir = resolve17(PLATFORM_ROOT, "plugins");
10878
+ const pluginsDir = resolve18(PLATFORM_ROOT, "plugins");
10629
10879
  if (!existsSync19(pluginsDir)) return [];
10630
10880
  let entries;
10631
10881
  try {
@@ -10636,7 +10886,7 @@ function computeDormantPlugins(accountId) {
10636
10886
  const out = [];
10637
10887
  for (const name of entries) {
10638
10888
  if (enabled.has(name)) continue;
10639
- const manifestPath = resolve17(pluginsDir, name, "PLUGIN.md");
10889
+ const manifestPath = resolve18(pluginsDir, name, "PLUGIN.md");
10640
10890
  if (!existsSync19(manifestPath)) continue;
10641
10891
  let manifest;
10642
10892
  try {
@@ -10655,7 +10905,7 @@ function computeDormantPlugins(accountId) {
10655
10905
 
10656
10906
  // server/routes/admin/claude-sessions.ts
10657
10907
  import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
10658
- import { resolve as resolve18 } from "path";
10908
+ import { resolve as resolve19 } from "path";
10659
10909
  function readTunnelState(brandConfigDir) {
10660
10910
  const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
10661
10911
  if (!existsSync20(statePath)) return null;
@@ -10671,10 +10921,10 @@ function readTunnelState(brandConfigDir) {
10671
10921
  }
10672
10922
  }
10673
10923
  function resolveTunnelUrl() {
10674
- const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? process.env.PLATFORM_ROOT ?? resolve18(process.cwd(), "..");
10924
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? process.env.PLATFORM_ROOT ?? resolve19(process.cwd(), "..");
10675
10925
  let brand;
10676
10926
  try {
10677
- brand = JSON.parse(readFileSync20(resolve18(platformRoot2, "config", "brand.json"), "utf-8"));
10927
+ brand = JSON.parse(readFileSync20(resolve19(platformRoot2, "config", "brand.json"), "utf-8"));
10678
10928
  } catch {
10679
10929
  return null;
10680
10930
  }
@@ -10922,68 +11172,6 @@ import { realpathSync as realpathSync4 } from "fs";
10922
11172
  import { basename as basename6, dirname as dirname5, join as join19, resolve as resolve21, sep as sep5 } from "path";
10923
11173
  import { Readable as Readable2 } from "stream";
10924
11174
 
10925
- // app/lib/data-path.ts
10926
- import { realpathSync as realpathSync3 } from "fs";
10927
- import { resolve as resolve19, normalize, sep as sep3, relative } from "path";
10928
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve19(process.cwd(), "../platform");
10929
- var DATA_ROOT = resolve19(PLATFORM_ROOT6, "..", "data");
10930
- var CLAUDE_UPLOADS_ROOT = process.env.CLAUDE_CONFIG_DIR ? resolve19(process.env.CLAUDE_CONFIG_DIR, "uploads") : null;
10931
- var ACCOUNT_PARTITION_DIRS = /* @__PURE__ */ new Set(["uploads", "accounts"]);
10932
- var ACCOUNT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10933
- function crossesForeignAccountPartition(relPath, accountId) {
10934
- const segs = relPath.split("/").filter(Boolean);
10935
- if (segs.length < 2) return false;
10936
- if (!ACCOUNT_PARTITION_DIRS.has(segs[0])) return false;
10937
- const owner = segs[1];
10938
- if (!ACCOUNT_UUID_RE.test(owner)) return false;
10939
- return owner !== accountId;
10940
- }
10941
- function resolveUnderRoot(raw, rootAbs, rootLabel) {
10942
- const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
10943
- const absolute = resolve19(rootAbs, cleaned);
10944
- let rootReal;
10945
- try {
10946
- rootReal = realpathSync3(rootAbs);
10947
- } catch (err) {
10948
- return {
10949
- ok: false,
10950
- status: 500,
10951
- error: `${rootLabel} not accessible: ${err instanceof Error ? err.message : String(err)}`
10952
- };
10953
- }
10954
- let resolvedReal;
10955
- try {
10956
- resolvedReal = realpathSync3(absolute);
10957
- } catch (err) {
10958
- const code = err.code;
10959
- if (code === "ENOENT") {
10960
- if (absolute !== rootReal && !absolute.startsWith(rootReal + sep3)) {
10961
- return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: absolute };
10962
- }
10963
- return { ok: false, status: 404, error: "Not found" };
10964
- }
10965
- return {
10966
- ok: false,
10967
- status: 500,
10968
- error: err instanceof Error ? err.message : String(err)
10969
- };
10970
- }
10971
- if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + sep3)) {
10972
- return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: resolvedReal };
10973
- }
10974
- const relPath = relative(rootReal, resolvedReal) || ".";
10975
- return { ok: true, absolute: resolvedReal, dataRootReal: rootReal, relative: relPath };
10976
- }
10977
- function resolveDataPath(raw) {
10978
- return resolveUnderRoot(raw, DATA_ROOT, "DATA_ROOT");
10979
- }
10980
- function resolveClaudeUploadsPath(raw) {
10981
- if (!CLAUDE_UPLOADS_ROOT) {
10982
- return { ok: false, status: 404, error: "Not found" };
10983
- }
10984
- return resolveUnderRoot(raw, CLAUDE_UPLOADS_ROOT, "CLAUDE_UPLOADS_ROOT");
10985
- }
10986
-
10987
11175
  // ../lib/graph-trash/src/index.ts
10988
11176
  var UNIQUE_KEYS_BY_LABEL = {
10989
11177
  Person: ["email", "telephone"],
@@ -11221,11 +11409,11 @@ async function restoreNode(params) {
11221
11409
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11222
11410
  function parseAttachmentPath(relPath) {
11223
11411
  const segments = relPath.split("/").filter(Boolean);
11224
- if (segments.length !== 4) return null;
11225
- if (segments[0] !== "uploads") return null;
11412
+ if (segments.length !== 5) return null;
11413
+ if (segments[0] !== "accounts" || segments[2] !== "uploads") return null;
11226
11414
  const accountId = segments[1];
11227
- const attachmentId = segments[2];
11228
- const filename = segments[3];
11415
+ const attachmentId = segments[3];
11416
+ const filename = segments[4];
11229
11417
  if (!UUID_RE2.test(accountId) || !UUID_RE2.test(attachmentId)) return null;
11230
11418
  const dot = filename.lastIndexOf(".");
11231
11419
  if (dot === -1) return null;
@@ -11303,6 +11491,17 @@ import { resolve as resolve20, relative as relative2, join as join18, basename a
11303
11491
  import { tmpdir as tmpdir2 } from "os";
11304
11492
  import { execFile as execFile2 } from "child_process";
11305
11493
  import { promisify as promisify2 } from "util";
11494
+
11495
+ // app/lib/migration-state.ts
11496
+ var inProgress = false;
11497
+ function setUploadsMigrationInProgress(value) {
11498
+ inProgress = value;
11499
+ }
11500
+ function uploadsMigrationInProgress() {
11501
+ return inProgress;
11502
+ }
11503
+
11504
+ // app/lib/file-index.ts
11306
11505
  var execFileAsync2 = promisify2(execFile2);
11307
11506
  var CONTENT_CAP = 1e5;
11308
11507
  var OCR_TIMEOUT_MS = 3e4;
@@ -11607,7 +11806,6 @@ async function reconcileFileIndex(accountId, opts) {
11607
11806
  return withSession(opts, async (session) => {
11608
11807
  const walked = [];
11609
11808
  const subtreeRoots = [
11610
- resolve20(dataRoot, "uploads", accountId),
11611
11809
  resolve20(dataRoot, "accounts", accountId)
11612
11810
  ];
11613
11811
  let clean = true;
@@ -11644,7 +11842,9 @@ async function reconcileFileIndex(accountId, opts) {
11644
11842
  }
11645
11843
  let pruned = 0;
11646
11844
  let cascadeKds = 0, cascadeSections = 0, cascadeChunks = 0;
11647
- if (clean) {
11845
+ if (clean && uploadsMigrationInProgress()) {
11846
+ console.error(`[file-index] reconcile account=${accountId} prune-skipped reason="uploads-migration-in-progress"`);
11847
+ } else if (clean) {
11648
11848
  const foundPaths = walked.map((w) => w.relativePath);
11649
11849
  const goneRes = await session.run(
11650
11850
  `MATCH (f:FileArtifact {accountId: $accountId})
@@ -11804,6 +12004,32 @@ function sortDirEntries(entries) {
11804
12004
  return aKey.localeCompare(bKey);
11805
12005
  });
11806
12006
  }
12007
+ var PROTECTED_DELETE_SEGMENTS = {
12008
+ ".git": "git",
12009
+ ".claude": "claude",
12010
+ secrets: "secrets",
12011
+ agents: "agents",
12012
+ specialists: "specialists",
12013
+ state: "state"
12014
+ };
12015
+ var PROTECTED_DELETE_BASENAMES = {
12016
+ "account.json": "account-json",
12017
+ "wa-channel-bindings.json": "channel-bindings",
12018
+ "webchat-channel-bindings.json": "channel-bindings"
12019
+ };
12020
+ function isProtectedFromDeletion(relPath) {
12021
+ const segments = relPath.split("/").filter(Boolean);
12022
+ for (const seg of segments) {
12023
+ if (Object.hasOwn(PROTECTED_DELETE_SEGMENTS, seg)) {
12024
+ return { protected: true, rule: PROTECTED_DELETE_SEGMENTS[seg] };
12025
+ }
12026
+ }
12027
+ const base = segments[segments.length - 1];
12028
+ if (base && Object.hasOwn(PROTECTED_DELETE_BASENAMES, base)) {
12029
+ return { protected: true, rule: PROTECTED_DELETE_BASENAMES[base] };
12030
+ }
12031
+ return { protected: false };
12032
+ }
11807
12033
  async function knowledgeDocOwnedByAccount(accountId, attachmentId) {
11808
12034
  if (!attachmentId) return false;
11809
12035
  const session = getSession();
@@ -11849,7 +12075,7 @@ app19.get("/", requireAdminSession, async (c) => {
11849
12075
  }
11850
12076
  const names = await readdir3(absolute);
11851
12077
  const segs = relPath.split("/").filter(Boolean);
11852
- const isAccountParent = segs.length === 1 && (segs[0] === "accounts" || segs[0] === "uploads");
12078
+ const isAccountParent = segs.length === 1 && segs[0] === "accounts";
11853
12079
  const entries = [];
11854
12080
  for (const name of names) {
11855
12081
  if (UUID_RE3.test(name.replace(/\.meta\.json$/, "")) && name.endsWith(".meta.json")) {
@@ -11858,6 +12084,8 @@ app19.get("/", requireAdminSession, async (c) => {
11858
12084
  if (isAccountParent && UUID_RE3.test(name) && name !== accountId) {
11859
12085
  continue;
11860
12086
  }
12087
+ const childRel = relPath === "" || relPath === "." ? name : `${relPath}/${name}`;
12088
+ const protectedEntry = isProtectedFromDeletion(childRel).protected;
11861
12089
  try {
11862
12090
  const entryPath = join19(absolute, name);
11863
12091
  const s = await stat4(entryPath);
@@ -11865,10 +12093,11 @@ app19.get("/", requireAdminSession, async (c) => {
11865
12093
  name,
11866
12094
  kind: s.isDirectory() ? "directory" : s.isFile() ? "file" : "other",
11867
12095
  sizeBytes: s.isDirectory() ? null : s.size,
11868
- modifiedAt: s.mtime.toISOString()
12096
+ modifiedAt: s.mtime.toISOString(),
12097
+ protected: protectedEntry
11869
12098
  });
11870
12099
  } catch {
11871
- entries.push({ name, kind: "other", sizeBytes: null, modifiedAt: (/* @__PURE__ */ new Date(0)).toISOString() });
12100
+ entries.push({ name, kind: "other", sizeBytes: null, modifiedAt: (/* @__PURE__ */ new Date(0)).toISOString(), protected: protectedEntry });
11872
12101
  }
11873
12102
  }
11874
12103
  const accountNames = await readAccountNames();
@@ -11984,14 +12213,14 @@ app19.post("/upload", requireAdminSession, async (c) => {
11984
12213
  }
11985
12214
  const safeName = basename6(file.name).replace(/[\0/\\]/g, "_");
11986
12215
  const finalName = `${Date.now()}-${safeName}`;
11987
- const destDir = resolve21(DATA_ROOT, "uploads", accountId);
12216
+ const destDir = resolve21(DATA_ROOT, "accounts", accountId, "uploads");
11988
12217
  const destPath = resolve21(destDir, finalName);
11989
12218
  try {
11990
12219
  await mkdir3(destDir, { recursive: true });
11991
12220
  const dataRootReal = realpathSync4(DATA_ROOT);
11992
12221
  const destDirReal = realpathSync4(destDir);
11993
12222
  if (destDirReal !== dataRootReal && !destDirReal.startsWith(dataRootReal + sep5)) {
11994
- console.error(`[data] path-traversal-blocked requested="uploads/${accountId}" resolved="${destDirReal}"`);
12223
+ console.error(`[data] path-traversal-blocked requested="accounts/${accountId}/uploads" resolved="${destDirReal}"`);
11995
12224
  return c.json({ error: "Upload destination escapes DATA_ROOT" }, 403);
11996
12225
  }
11997
12226
  const buffer = Buffer.from(await file.arrayBuffer());
@@ -12001,7 +12230,7 @@ app19.post("/upload", requireAdminSession, async (c) => {
12001
12230
  console.error(`[data] file-upload error filename="${safeName}" err="${message}"`);
12002
12231
  return c.json({ error: message }, 500);
12003
12232
  }
12004
- const relativeDest = `uploads/${accountId}/${finalName}`;
12233
+ const relativeDest = `accounts/${accountId}/uploads/${finalName}`;
12005
12234
  console.error(`[data] file-upload filename="${safeName}" size=${file.size} dest="${relativeDest}"`);
12006
12235
  void indexFile(accountId, relativeDest).catch((err) => {
12007
12236
  console.error(`[data] file-upload index-failed dest="${relativeDest}" err="${err instanceof Error ? err.message : String(err)}"`);
@@ -12035,9 +12264,9 @@ app19.delete("/", requireAdminSession, async (c) => {
12035
12264
  return c.json({ error: "Not found" }, 404);
12036
12265
  }
12037
12266
  const base = basename6(absolute);
12038
- const segments = relPath.split("/").filter(Boolean);
12039
- if (base === "account.json" || segments.includes(".git")) {
12040
- console.error(`[data] file-delete blocked path="${relPath}" reason="protected"`);
12267
+ const protection = isProtectedFromDeletion(relPath);
12268
+ if (protection.protected) {
12269
+ console.error(`[data] file-delete blocked path="${relPath}" reason="protected" rule="${protection.rule}"`);
12041
12270
  return c.json({ error: "Protected file \u2014 refusing to delete" }, 403);
12042
12271
  }
12043
12272
  try {
@@ -14136,13 +14365,14 @@ async function fetchAccountFileArtefacts(accountId) {
14136
14365
  const result = await session.run(
14137
14366
  `MATCH (f:FileArtifact { accountId: $accountId })
14138
14367
  WHERE f.relativePath STARTS WITH 'accounts/'
14368
+ AND NOT f.relativePath STARTS WITH $uploadsPrefix
14139
14369
  RETURN f.relativePath AS relativePath,
14140
14370
  f.displayName AS displayName,
14141
14371
  f.mimeType AS mimeType,
14142
14372
  f.modifiedAt AS modifiedAt
14143
14373
  ORDER BY f.modifiedAt DESC
14144
14374
  LIMIT 50`,
14145
- { accountId }
14375
+ { accountId, uploadsPrefix: `accounts/${accountId}/uploads/` }
14146
14376
  );
14147
14377
  return result.records.map((r) => {
14148
14378
  const relativePath = r.get("relativePath");
@@ -14398,7 +14628,7 @@ var session_archive_default = app29;
14398
14628
 
14399
14629
  // server/routes/admin/session-rc-spawn.ts
14400
14630
  import { randomUUID as randomUUID10 } from "crypto";
14401
- function resolveSpawnPlan(host, operatorDomains, body, mintId = randomUUID10) {
14631
+ function resolveSpawnPlan(host, operatorDomains, body, mintId = randomUUID10, adminUserId) {
14402
14632
  const operator = isOperatorHost(host, operatorDomains);
14403
14633
  const resumeId = typeof body.sessionId === "string" && body.sessionId.length > 0 ? body.sessionId : void 0;
14404
14634
  const name = typeof body.name === "string" && body.name.length > 0 ? body.name : void 0;
@@ -14416,6 +14646,7 @@ function resolveSpawnPlan(host, operatorDomains, body, mintId = randomUUID10) {
14416
14646
  payload.senderId = key;
14417
14647
  payload.webchatChannel = webchatChannelDescriptor(key);
14418
14648
  if (name) payload.name = name;
14649
+ if (adminUserId) payload.adminUserId = adminUserId;
14419
14650
  } else {
14420
14651
  sessionId = resumeId;
14421
14652
  payload.sessionId = resumeId;
@@ -14436,7 +14667,9 @@ app30.post("/", requireAdminSession, async (c) => {
14436
14667
  body = {};
14437
14668
  }
14438
14669
  const host = (c.req.header("host") ?? "").split(":")[0];
14439
- const plan = resolveSpawnPlan(host, getOperatorDomains(), body);
14670
+ const cacheKey = c.get("cacheKey");
14671
+ const adminUserId = cacheKey ? getUserIdForSession(cacheKey) : void 0;
14672
+ const plan = resolveSpawnPlan(host, getOperatorDomains(), body, randomUUID10, adminUserId);
14440
14673
  const spawnReqId = randomUUID10();
14441
14674
  console.log(
14442
14675
  `[chat-spawn] op=request spawnReqId=${spawnReqId} origin=${plan.origin} kind=${plan.kind} sessionId=${plan.kind === "new" ? "new" : plan.sessionId}`
@@ -14605,9 +14838,9 @@ var system_stats_default = app31;
14605
14838
  // server/routes/admin/health.ts
14606
14839
  import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
14607
14840
  import { resolve as resolve23, join as join20 } from "path";
14608
- var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve23(process.cwd(), "..");
14841
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve23(process.cwd(), "..");
14609
14842
  var brandHostname = "maxy";
14610
- var brandJsonPath = join20(PLATFORM_ROOT7, "config", "brand.json");
14843
+ var brandJsonPath = join20(PLATFORM_ROOT6, "config", "brand.json");
14611
14844
  if (existsSync22(brandJsonPath)) {
14612
14845
  try {
14613
14846
  const brand = JSON.parse(readFileSync21(brandJsonPath, "utf-8"));
@@ -14615,7 +14848,7 @@ if (existsSync22(brandJsonPath)) {
14615
14848
  } catch {
14616
14849
  }
14617
14850
  }
14618
- var VERSION_FILE = resolve23(PLATFORM_ROOT7, `config/.${brandHostname}-version`);
14851
+ var VERSION_FILE = resolve23(PLATFORM_ROOT6, `config/.${brandHostname}-version`);
14619
14852
  var PROCESS_STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
14620
14853
  var PROBE_TIMEOUT_MS = 1e3;
14621
14854
  function readVersion() {
@@ -15325,9 +15558,9 @@ var verify_token_default = app41;
15325
15558
  // app/lib/access-email.ts
15326
15559
  import { spawn as spawn2 } from "child_process";
15327
15560
  import { resolve as resolve24 } from "path";
15328
- var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
15561
+ var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
15329
15562
  var SEND_SCRIPT = resolve24(
15330
- PLATFORM_ROOT8,
15563
+ PLATFORM_ROOT7,
15331
15564
  "plugins",
15332
15565
  "email",
15333
15566
  "mcp",
@@ -16563,7 +16796,7 @@ import * as fsp2 from "fs/promises";
16563
16796
  import { resolve as resolve27, sep as sep7 } from "path";
16564
16797
  var ACCOUNT_UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16565
16798
  var DEFAULT_COALESCE_MS = 500;
16566
- var ROOTS = ["uploads", "accounts"];
16799
+ var ROOTS = ["accounts"];
16567
16800
  function _routeEvent(rootName, filename) {
16568
16801
  if (!filename) return null;
16569
16802
  const segs = filename.split(sep7).filter(Boolean);
@@ -16661,6 +16894,129 @@ async function startFileWatcher(opts = {}) {
16661
16894
  return { stop };
16662
16895
  }
16663
16896
 
16897
+ // app/lib/migrate-uploads.ts
16898
+ import { mkdir as mkdir5, readdir as readdir5, rename, rm as rm3 } from "fs/promises";
16899
+ import { dirname as dirname7, relative as relative4, resolve as resolve28 } from "path";
16900
+ var ACCOUNT_UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16901
+ async function walkFiles(dir) {
16902
+ const out = [];
16903
+ let entries;
16904
+ try {
16905
+ entries = await readdir5(dir, { withFileTypes: true });
16906
+ } catch {
16907
+ return out;
16908
+ }
16909
+ for (const e of entries) {
16910
+ const abs = resolve28(dir, e.name);
16911
+ if (e.isDirectory()) {
16912
+ out.push(...await walkFiles(abs));
16913
+ } else if (e.isFile()) {
16914
+ out.push(abs);
16915
+ }
16916
+ }
16917
+ return out;
16918
+ }
16919
+ function toNumber2(v) {
16920
+ if (v && typeof v.toNumber === "function") {
16921
+ return v.toNumber();
16922
+ }
16923
+ return Number(v ?? 0);
16924
+ }
16925
+ async function relocateTree(srcDir, destDir, dataRoot, accountId, session) {
16926
+ const files = await walkFiles(srcDir);
16927
+ let moved = 0;
16928
+ for (const oldAbs of files) {
16929
+ const suffix = relative4(srcDir, oldAbs);
16930
+ const newAbs = resolve28(destDir, suffix);
16931
+ await mkdir5(dirname7(newAbs), { recursive: true });
16932
+ await rename(oldAbs, newAbs);
16933
+ moved++;
16934
+ const oldRel = relative4(dataRoot, oldAbs);
16935
+ const newRel = relative4(dataRoot, newAbs);
16936
+ let linked = false;
16937
+ if (accountId && session) {
16938
+ const fa = await session.run(
16939
+ `MATCH (f:FileArtifact {accountId: $accountId, relativePath: $oldRel})
16940
+ SET f.relativePath = $newRel RETURN count(f) AS n`,
16941
+ { accountId, oldRel, newRel }
16942
+ );
16943
+ const kd = await session.run(
16944
+ `MATCH (d:KnowledgeDocument {accountId: $accountId, sourcePath: $oldAbs})
16945
+ SET d.sourcePath = $newAbs RETURN count(d) AS n`,
16946
+ { accountId, oldAbs, newAbs }
16947
+ );
16948
+ linked = toNumber2(fa.records[0]?.get("n")) + toNumber2(kd.records[0]?.get("n")) > 0;
16949
+ }
16950
+ console.error(`[uploads-migrate] from=${oldRel} to=${newRel} linked=${linked}`);
16951
+ }
16952
+ await rm3(srcDir, { recursive: true, force: true });
16953
+ return moved;
16954
+ }
16955
+ async function migrateUploads(opts = {}) {
16956
+ const dataRoot = opts.dataRoot ?? DATA_ROOT;
16957
+ const oldRoot = resolve28(dataRoot, "uploads");
16958
+ let topEntries;
16959
+ try {
16960
+ topEntries = await readdir5(oldRoot, { withFileTypes: true });
16961
+ } catch (err) {
16962
+ if (err.code === "ENOENT") {
16963
+ return { moved: 0, skipped: 0, removedRoot: false };
16964
+ }
16965
+ throw err;
16966
+ }
16967
+ const installAccountId = opts.installAccountId ?? resolveAccount()?.accountId;
16968
+ const session = opts.session ?? getSession();
16969
+ const ownSession = !opts.session;
16970
+ setUploadsMigrationInProgress(true);
16971
+ let moved = 0;
16972
+ let skipped = 0;
16973
+ try {
16974
+ for (const entry of topEntries) {
16975
+ const name = entry.name;
16976
+ if (ACCOUNT_UUID_RE3.test(name)) {
16977
+ moved += await relocateTree(
16978
+ resolve28(oldRoot, name),
16979
+ resolve28(dataRoot, "accounts", name, "uploads"),
16980
+ dataRoot,
16981
+ name,
16982
+ session
16983
+ );
16984
+ } else if (name === "public") {
16985
+ if (!installAccountId) {
16986
+ console.error(`[uploads-migrate] skip entry="public" reason="no-install-account"`);
16987
+ skipped++;
16988
+ continue;
16989
+ }
16990
+ moved += await relocateTree(
16991
+ resolve28(oldRoot, "public"),
16992
+ resolve28(dataRoot, "accounts", installAccountId, "uploads", "public"),
16993
+ dataRoot,
16994
+ null,
16995
+ // public uploads carry no graph nodes
16996
+ session
16997
+ );
16998
+ } else {
16999
+ console.error(`[uploads-migrate] skip entry="${name}" reason="not-account-or-public"`);
17000
+ skipped++;
17001
+ }
17002
+ }
17003
+ let removedRoot = false;
17004
+ try {
17005
+ const remaining = await readdir5(oldRoot);
17006
+ if (remaining.length === 0) {
17007
+ await rm3(oldRoot, { recursive: true, force: true });
17008
+ removedRoot = true;
17009
+ }
17010
+ } catch {
17011
+ }
17012
+ console.error(`[uploads-migrate] done moved=${moved} skipped=${skipped} removedRoot=${removedRoot}`);
17013
+ return { moved, skipped, removedRoot };
17014
+ } finally {
17015
+ setUploadsMigrationInProgress(false);
17016
+ if (ownSession) await session.close();
17017
+ }
17018
+ }
17019
+
16664
17020
  // app/lib/whatsapp/gateway/inbound-hub.ts
16665
17021
  var InboundHub = class {
16666
17022
  senders = /* @__PURE__ */ new Map();
@@ -17572,7 +17928,7 @@ function broadcastAdminShutdown(reason) {
17572
17928
  // ../lib/entitlement/src/index.ts
17573
17929
  import { createPublicKey, createHash as createHash5, verify as cryptoVerify } from "crypto";
17574
17930
  import { existsSync as existsSync26, readFileSync as readFileSync25, statSync as statSync10 } from "fs";
17575
- import { resolve as resolve28 } from "path";
17931
+ import { resolve as resolve29 } from "path";
17576
17932
 
17577
17933
  // ../lib/entitlement/src/canonicalize.ts
17578
17934
  function canonicalize(value) {
@@ -17607,7 +17963,7 @@ var PUBKEY_SHA256 = "8eee6bcb33545fd13b16d3199a5735ca5db5062834c7b49dfe4f23801d9
17607
17963
  var GRACE_DAYS = 7;
17608
17964
  var GRACE_MS = GRACE_DAYS * 24 * 60 * 60 * 1e3;
17609
17965
  function pubkeyPath(brand) {
17610
- return resolve28(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
17966
+ return resolve29(brand.platformRoot, "lib", "entitlement", "rubytech-pubkey.pem");
17611
17967
  }
17612
17968
  var memo = null;
17613
17969
  function memoKey(mtimeMs, account) {
@@ -17619,7 +17975,7 @@ function resolveEntitlement(brand, account) {
17619
17975
  if (brand.commercialMode !== true) {
17620
17976
  return logResolved(implicitTrust(account), null);
17621
17977
  }
17622
- const entitlementPath = resolve28(brand.configDir, "entitlement.json");
17978
+ const entitlementPath = resolve29(brand.configDir, "entitlement.json");
17623
17979
  if (!existsSync26(entitlementPath)) {
17624
17980
  return logResolved(anonymousFallback("missing"), { reason: "missing" });
17625
17981
  }
@@ -17809,8 +18165,8 @@ function clientFrom(c) {
17809
18165
  c.req.header("x-forwarded-for")
17810
18166
  );
17811
18167
  }
17812
- var PLATFORM_ROOT9 = process.env.MAXY_PLATFORM_ROOT || "";
17813
- var BRAND_JSON_PATH = PLATFORM_ROOT9 ? join22(PLATFORM_ROOT9, "config", "brand.json") : "";
18168
+ var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
18169
+ var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join22(PLATFORM_ROOT8, "config", "brand.json") : "";
17814
18170
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
17815
18171
  if (BRAND_JSON_PATH && !existsSync27(BRAND_JSON_PATH)) {
17816
18172
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -17868,7 +18224,7 @@ var app49 = new Hono();
17868
18224
  var nativeFileFollowers = /* @__PURE__ */ new Map();
17869
18225
  var waGateway = new WaGateway({
17870
18226
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
17871
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve29(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
18227
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve30(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
17872
18228
  // Task 751 — file delivery on the native channel: resolve the platform
17873
18229
  // account + path validation here and funnel through the shared send core.
17874
18230
  sendDocument: async ({ senderId, accountId, filePath, caption }) => {
@@ -17884,7 +18240,7 @@ var waGateway = new WaGateway({
17884
18240
  caption,
17885
18241
  accountId,
17886
18242
  maxyAccountId,
17887
- platformRoot: resolve29(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, ".."))
18243
+ platformRoot: resolve30(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, ".."))
17888
18244
  });
17889
18245
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
17890
18246
  },
@@ -18188,10 +18544,11 @@ app49.post("/api/remote-auth/set-password", async (c) => {
18188
18544
  });
18189
18545
  app49.route("/api/_client-error", client_error_default);
18190
18546
  console.log("[client-error-route] mounted");
18547
+ var PWA_PUBLIC_PATHS = /* @__PURE__ */ new Set(["/sw.js", ...PWA_SURFACES.map((s) => s.manifestPath)]);
18191
18548
  app49.use("*", async (c, next) => {
18192
18549
  const host = (c.req.header("host") ?? "").split(":")[0];
18193
18550
  const path2 = c.req.path;
18194
- if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/")) {
18551
+ if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/") || PWA_PUBLIC_PATHS.has(path2)) {
18195
18552
  await next();
18196
18553
  return;
18197
18554
  }
@@ -18266,8 +18623,8 @@ app49.get("/agent-assets/:slug/:filename", (c) => {
18266
18623
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
18267
18624
  return c.text("Not found", 404);
18268
18625
  }
18269
- const filePath = resolve29(account.accountDir, "agents", slug, "assets", filename);
18270
- const expectedDir = resolve29(account.accountDir, "agents", slug, "assets");
18626
+ const filePath = resolve30(account.accountDir, "agents", slug, "assets", filename);
18627
+ const expectedDir = resolve30(account.accountDir, "agents", slug, "assets");
18271
18628
  if (!filePath.startsWith(expectedDir + "/")) {
18272
18629
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
18273
18630
  return c.text("Forbidden", 403);
@@ -18296,8 +18653,8 @@ app49.get("/generated/:filename", (c) => {
18296
18653
  console.error(`[generated] serve file=${filename} status=404`);
18297
18654
  return c.text("Not found", 404);
18298
18655
  }
18299
- const filePath = resolve29(account.accountDir, "generated", filename);
18300
- const expectedDir = resolve29(account.accountDir, "generated");
18656
+ const filePath = resolve30(account.accountDir, "generated", filename);
18657
+ const expectedDir = resolve30(account.accountDir, "generated");
18301
18658
  if (!filePath.startsWith(expectedDir + "/")) {
18302
18659
  console.error(`[generated] serve file=${filename} status=403`);
18303
18660
  return c.text("Forbidden", 403);
@@ -18338,10 +18695,21 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
18338
18695
  icon: brandIconPath,
18339
18696
  logoContainsName: !!BRAND.logoContainsName
18340
18697
  })}</script>`;
18698
+ var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
18699
+ var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
18700
+ var brandIconFsPath = resolve30(process.cwd(), "public", brandIconPath.replace(/^\//, ""));
18701
+ var brandIconExists = existsSync27(brandIconFsPath);
18702
+ var SW_SOURCE = (() => {
18703
+ try {
18704
+ return readFileSync26(resolve30(process.cwd(), "public", "sw.js"), "utf-8");
18705
+ } catch {
18706
+ return null;
18707
+ }
18708
+ })();
18341
18709
  function readInstalledVersion() {
18342
18710
  try {
18343
- if (!PLATFORM_ROOT9) return "unknown";
18344
- const versionFile = join22(PLATFORM_ROOT9, "config", `.${BRAND.hostname}-version`);
18711
+ if (!PLATFORM_ROOT8) return "unknown";
18712
+ const versionFile = join22(PLATFORM_ROOT8, "config", `.${BRAND.hostname}-version`);
18345
18713
  if (!existsSync27(versionFile)) return "unknown";
18346
18714
  const content = readFileSync26(versionFile, "utf-8").trim();
18347
18715
  return content || "unknown";
@@ -18384,15 +18752,17 @@ var clientErrorReporterScript = `<script>
18384
18752
  function cachedHtml(file) {
18385
18753
  let html = htmlCache.get(file);
18386
18754
  if (!html) {
18387
- html = readFileSync26(resolve29(process.cwd(), "public", file), "utf-8");
18755
+ html = readFileSync26(resolve30(process.cwd(), "public", file), "utf-8");
18388
18756
  const productNameEsc = escapeHtml(BRAND.productName);
18389
18757
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
18390
18758
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
18391
- const headInjection = file === "index.html" ? `${brandScript}
18759
+ const pwaSurface = pwaSurfaceForShell(file);
18760
+ const pwaHead = pwaSurface ? "\n" + pwaHeadTags(pwaSurface, { iconPath: brandIconPath, themeColor: brandThemeColor }) + "\n" : "";
18761
+ const headInjection = (file === "index.html" ? `${brandScript}
18392
18762
  ${versionScript}
18393
18763
  ${clientErrorReporterScript}
18394
18764
  ` : `${brandScript}
18395
- `;
18765
+ `) + pwaHead;
18396
18766
  html = html.replace("</head>", `${headInjection}</head>`);
18397
18767
  htmlCache.set(file, html);
18398
18768
  }
@@ -18500,7 +18870,7 @@ app49.use("/vnc-popout.html", logViewerFetch);
18500
18870
  app49.get("/vnc-popout.html", (c) => {
18501
18871
  let html = htmlCache.get("vnc-popout.html");
18502
18872
  if (!html) {
18503
- html = readFileSync26(resolve29(process.cwd(), "public", "vnc-popout.html"), "utf-8");
18873
+ html = readFileSync26(resolve30(process.cwd(), "public", "vnc-popout.html"), "utf-8");
18504
18874
  const name = escapeHtml(BRAND.productName);
18505
18875
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
18506
18876
  html = html.replace("</head>", ` ${brandScript}
@@ -18534,6 +18904,30 @@ app49.post("/api/vnc/client-event", async (c) => {
18534
18904
  app49.get("/g/:slug", (c) => {
18535
18905
  return c.html(brandedPublicHtml());
18536
18906
  });
18907
+ for (const pwa of PWA_SURFACES) {
18908
+ app49.get(pwa.manifestPath, (c) => {
18909
+ const manifest = buildManifest(pwa, {
18910
+ productName: BRAND.productName,
18911
+ iconPath: brandIconPath,
18912
+ themeColor: brandThemeColor,
18913
+ backgroundColor: brandBackgroundColor
18914
+ });
18915
+ c.header("Content-Type", MANIFEST_CONTENT_TYPE);
18916
+ console.log(
18917
+ `[pwa] op=manifest surface=${pwa.surface} brand=${BRAND.productName} status=200 ct=${MANIFEST_CONTENT_TYPE} icon=${brandIconPath} iconExists=${brandIconExists}`
18918
+ );
18919
+ return c.body(JSON.stringify(manifest));
18920
+ });
18921
+ }
18922
+ app49.get("/sw.js", (c) => {
18923
+ if (SW_SOURCE == null) {
18924
+ console.error("[pwa] op=sw status=500 reason=sw-source-missing");
18925
+ return c.text("Service worker unavailable", 500);
18926
+ }
18927
+ c.header("Content-Type", SW_CONTENT_TYPE);
18928
+ console.log(`[pwa] op=sw status=200 ct=${SW_CONTENT_TYPE}`);
18929
+ return c.body(SW_SOURCE);
18930
+ });
18537
18931
  app49.get("/graph", (c) => {
18538
18932
  const host = (c.req.header("host") ?? "").split(":")[0];
18539
18933
  if (isPublicHost(host) || isOperatorHost(host, getOperatorDomains())) return c.text("Not found", 404);
@@ -18591,6 +18985,21 @@ var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
18591
18985
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
18592
18986
  var httpServer = serve({ fetch: app49.fetch, port, hostname });
18593
18987
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
18988
+ {
18989
+ const census = pwaCensus({
18990
+ shellHtml: (file) => {
18991
+ try {
18992
+ return cachedHtml(file);
18993
+ } catch {
18994
+ return null;
18995
+ }
18996
+ },
18997
+ iconExists: () => brandIconExists
18998
+ });
18999
+ console.log(
19000
+ `[pwa-census] surfaces=${census.surfaces} manifest-linked=${census.manifestLinked} icons-present=${census.iconsPresent}`
19001
+ );
19002
+ }
18594
19003
  startAuthHealthHeartbeat();
18595
19004
  {
18596
19005
  const loopHist = monitorEventLoopDelay({ resolution: 50 });
@@ -18687,8 +19096,10 @@ var adminUserReconcileTimer = setInterval(() => {
18687
19096
  }, ADMINUSER_RECONCILE_INTERVAL_MS);
18688
19097
  if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
18689
19098
  startGraphHealthTimer();
18690
- void startFileWatcher().catch((err) => {
18691
- console.error(`[file-watcher] start-failed err="${err instanceof Error ? err.message : String(err)}"`);
19099
+ void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
19100
+ void startFileWatcher().catch((err) => {
19101
+ console.error(`[file-watcher] start-failed err="${err instanceof Error ? err.message : String(err)}"`);
19102
+ });
18692
19103
  });
18693
19104
  try {
18694
19105
  const accounts = enumerateValidAccountIds(getAccountsDirFromEnv());
@@ -18770,7 +19181,7 @@ if (bootAccountConfig?.whatsapp) {
18770
19181
  }
18771
19182
  init({
18772
19183
  configDir: configDirForWhatsApp,
18773
- platformRoot: resolve29(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, "..")),
19184
+ platformRoot: resolve30(process.env.MAXY_PLATFORM_ROOT ?? join22(__dirname, "..")),
18774
19185
  accountConfig: bootAccountConfig,
18775
19186
  onMessage: async (msg) => {
18776
19187
  if (msg.isOwnerMirror) {