termdock 1.4.231 → 1.4.233

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 (321) hide show
  1. package/.desktop-dist/main.js +9 -0
  2. package/dist/client/assets/{App-Bl-MnHlK.js → App-CBhXXSRZ.js} +24 -24
  3. package/dist/client/assets/App-CBhXXSRZ.js.br +0 -0
  4. package/dist/client/assets/App-CBhXXSRZ.js.gz +0 -0
  5. package/dist/client/assets/{ChangeWalkthroughPanel-oVCcuJL3.js → ChangeWalkthroughPanel-CeOq4iWF.js} +1 -1
  6. package/dist/client/assets/ChangeWalkthroughPanel-CeOq4iWF.js.br +0 -0
  7. package/dist/client/assets/ChangeWalkthroughPanel-CeOq4iWF.js.gz +0 -0
  8. package/dist/client/assets/{DagPlayground-B33ZJSNC.js → DagPlayground-nzwoofiL.js} +1 -1
  9. package/dist/client/assets/DagPlayground-nzwoofiL.js.br +0 -0
  10. package/dist/client/assets/DagPlayground-nzwoofiL.js.gz +0 -0
  11. package/dist/client/assets/{DiffLab-CZRP_SqH.js → DiffLab-BA5JJf_P.js} +1 -1
  12. package/dist/client/assets/DiffLab-BA5JJf_P.js.br +0 -0
  13. package/dist/client/assets/DiffLab-BA5JJf_P.js.gz +0 -0
  14. package/dist/client/assets/{DiffReview-BheUk7E5.js → DiffReview-CNnJIPtm.js} +1 -1
  15. package/dist/client/assets/DiffReview-CNnJIPtm.js.br +0 -0
  16. package/dist/client/assets/DiffReview-CNnJIPtm.js.gz +0 -0
  17. package/dist/client/assets/{DiffReviewLab-DupHSbbd.js → DiffReviewLab-CH9k1x7c.js} +1 -1
  18. package/dist/client/assets/DiffReviewLab-CH9k1x7c.js.br +0 -0
  19. package/dist/client/assets/DiffReviewLab-CH9k1x7c.js.gz +0 -0
  20. package/dist/client/assets/{DiffViewer-Cd5EE8E9.js → DiffViewer-D17tBMWn.js} +1 -1
  21. package/dist/client/assets/DiffViewer-D17tBMWn.js.br +0 -0
  22. package/dist/client/assets/DiffViewer-D17tBMWn.js.gz +0 -0
  23. package/dist/client/assets/{ModelPreview-Fnuxnt8A.js → ModelPreview-eUZEjzgg.js} +1 -1
  24. package/dist/client/assets/ModelPreview-eUZEjzgg.js.br +0 -0
  25. package/dist/client/assets/ModelPreview-eUZEjzgg.js.gz +0 -0
  26. package/dist/client/assets/{RightSidebar-DTTmdNhf.js → RightSidebar-DQSjBvAA.js} +4 -4
  27. package/dist/client/assets/RightSidebar-DQSjBvAA.js.br +0 -0
  28. package/dist/client/assets/RightSidebar-DQSjBvAA.js.gz +0 -0
  29. package/dist/client/assets/{abnfDiagram-VCTEODGH-1oyW4gyC.js → abnfDiagram-VCTEODGH-CeIsieUL.js} +1 -1
  30. package/dist/client/assets/abnfDiagram-VCTEODGH-CeIsieUL.js.br +0 -0
  31. package/dist/client/assets/abnfDiagram-VCTEODGH-CeIsieUL.js.gz +0 -0
  32. package/dist/client/assets/app-CM_Chr9o.js +35 -0
  33. package/dist/client/assets/app-CM_Chr9o.js.br +0 -0
  34. package/dist/client/assets/app-CM_Chr9o.js.gz +0 -0
  35. package/dist/client/assets/{arc-e9P469h4.js → arc-B0iWWfl8.js} +1 -1
  36. package/dist/client/assets/arc-B0iWWfl8.js.br +0 -0
  37. package/dist/client/assets/arc-B0iWWfl8.js.gz +0 -0
  38. package/dist/client/assets/{architectureDiagram-5GKGNRK7-CloNCKLm.js → architectureDiagram-5GKGNRK7-IV8_iAgW.js} +1 -1
  39. package/dist/client/assets/architectureDiagram-5GKGNRK7-IV8_iAgW.js.br +0 -0
  40. package/dist/client/assets/architectureDiagram-5GKGNRK7-IV8_iAgW.js.gz +0 -0
  41. package/dist/client/assets/{blockDiagram-I7D4REHJ-D3wH4hOJ.js → blockDiagram-I7D4REHJ-DWdbwev4.js} +1 -1
  42. package/dist/client/assets/blockDiagram-I7D4REHJ-DWdbwev4.js.br +0 -0
  43. package/dist/client/assets/blockDiagram-I7D4REHJ-DWdbwev4.js.gz +0 -0
  44. package/dist/client/assets/{c4Diagram-7LVT6UL2-CX5tSHre.js → c4Diagram-7LVT6UL2-DL6qM9qX.js} +1 -1
  45. package/dist/client/assets/{c4Diagram-7LVT6UL2-CX5tSHre.js.br → c4Diagram-7LVT6UL2-DL6qM9qX.js.br} +0 -0
  46. package/dist/client/assets/c4Diagram-7LVT6UL2-DL6qM9qX.js.gz +0 -0
  47. package/dist/client/assets/channel-CanBPcbJ.js +1 -0
  48. package/dist/client/assets/{chunk-2Q5K7J3B-Weq7o_yW.js → chunk-2Q5K7J3B-CADcnv26.js} +1 -1
  49. package/dist/client/assets/{chunk-5VM5RSS4-B1O3hmJE.js → chunk-5VM5RSS4-Dy92AQjN.js} +1 -1
  50. package/dist/client/assets/{chunk-F27PBJKO-BRYx-Dtz.js → chunk-F27PBJKO-CXMkCSL3.js} +1 -1
  51. package/dist/client/assets/chunk-F27PBJKO-CXMkCSL3.js.br +0 -0
  52. package/dist/client/assets/chunk-F27PBJKO-CXMkCSL3.js.gz +0 -0
  53. package/dist/client/assets/{chunk-IMKFNOWR-z6lXRY7P.js → chunk-IMKFNOWR-BHb_PEkZ.js} +1 -1
  54. package/dist/client/assets/chunk-IMKFNOWR-BHb_PEkZ.js.br +0 -0
  55. package/dist/client/assets/chunk-IMKFNOWR-BHb_PEkZ.js.gz +0 -0
  56. package/dist/client/assets/{chunk-JWPE2WC7-DdDHAlPc.js → chunk-JWPE2WC7-B4h9BTig.js} +1 -1
  57. package/dist/client/assets/{chunk-POPQ4Y6H-BYbFuciT.js → chunk-POPQ4Y6H-DMc2ZxT_.js} +1 -1
  58. package/dist/client/assets/{chunk-SVP7TREG-Bk-X0NaV.js → chunk-SVP7TREG-D2bHA6KO.js} +1 -1
  59. package/dist/client/assets/chunk-SVP7TREG-D2bHA6KO.js.br +0 -0
  60. package/dist/client/assets/chunk-SVP7TREG-D2bHA6KO.js.gz +0 -0
  61. package/dist/client/assets/{chunk-TICWLB2K-BjUqsphB.js → chunk-TICWLB2K-CSV5x8Yz.js} +1 -1
  62. package/dist/client/assets/chunk-TICWLB2K-CSV5x8Yz.js.br +0 -0
  63. package/dist/client/assets/chunk-TICWLB2K-CSV5x8Yz.js.gz +0 -0
  64. package/dist/client/assets/{chunk-XXDRQBXY-DSXlNway.js → chunk-XXDRQBXY-CEkyVIOo.js} +1 -1
  65. package/dist/client/assets/classDiagram-ZZMXUADV-Dbb9V726.js +1 -0
  66. package/dist/client/assets/classDiagram-v2-VYDZK3BY-Dbb9V726.js +1 -0
  67. package/dist/client/assets/{cose-bilkent-JH36ORCC-Bw8F9aUq.js → cose-bilkent-JH36ORCC-CUiXAbSC.js} +1 -1
  68. package/dist/client/assets/cose-bilkent-JH36ORCC-CUiXAbSC.js.br +0 -0
  69. package/dist/client/assets/cose-bilkent-JH36ORCC-CUiXAbSC.js.gz +0 -0
  70. package/dist/client/assets/{cynefin-OW5HDTMX-C5oB4ZTA.js → cynefin-OW5HDTMX-BgqqNrml.js} +1 -1
  71. package/dist/client/assets/cynefin-OW5HDTMX-BgqqNrml.js.br +0 -0
  72. package/dist/client/assets/cynefin-OW5HDTMX-BgqqNrml.js.gz +0 -0
  73. package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-BkbwVdAD.js → cynefinDiagram-5FMLGOSQ-CHJg73sm.js} +1 -1
  74. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CHJg73sm.js.br +0 -0
  75. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CHJg73sm.js.gz +0 -0
  76. package/dist/client/assets/{dagre-GXQ25YYZ-8Jt1so7H.js → dagre-GXQ25YYZ-Du6cYIDp.js} +1 -1
  77. package/dist/client/assets/dagre-GXQ25YYZ-Du6cYIDp.js.br +0 -0
  78. package/dist/client/assets/dagre-GXQ25YYZ-Du6cYIDp.js.gz +0 -0
  79. package/dist/client/assets/{diagram-S7CK7UJ4-DyWUtoUO.js → diagram-S7CK7UJ4-CNHI3hSO.js} +1 -1
  80. package/dist/client/assets/diagram-S7CK7UJ4-CNHI3hSO.js.br +0 -0
  81. package/dist/client/assets/diagram-S7CK7UJ4-CNHI3hSO.js.gz +0 -0
  82. package/dist/client/assets/{diagram-UQ7AKVKN-BBnqsz2l.js → diagram-UQ7AKVKN-BP2TAlt5.js} +1 -1
  83. package/dist/client/assets/diagram-UQ7AKVKN-BP2TAlt5.js.br +0 -0
  84. package/dist/client/assets/diagram-UQ7AKVKN-BP2TAlt5.js.gz +0 -0
  85. package/dist/client/assets/{diagram-VSXAHHWV-DQ2JxOjW.js → diagram-VSXAHHWV-BiNbCPl8.js} +1 -1
  86. package/dist/client/assets/diagram-VSXAHHWV-BiNbCPl8.js.br +0 -0
  87. package/dist/client/assets/diagram-VSXAHHWV-BiNbCPl8.js.gz +0 -0
  88. package/dist/client/assets/{diagram-VX7I27RA-Das3jLN9.js → diagram-VX7I27RA-Doe3bOA3.js} +1 -1
  89. package/dist/client/assets/diagram-VX7I27RA-Doe3bOA3.js.br +0 -0
  90. package/dist/client/assets/diagram-VX7I27RA-Doe3bOA3.js.gz +0 -0
  91. package/dist/client/assets/{diagram-Z3DM3KII-0gbBMfrq.js → diagram-Z3DM3KII-CRWmdH0s.js} +1 -1
  92. package/dist/client/assets/diagram-Z3DM3KII-CRWmdH0s.js.br +0 -0
  93. package/dist/client/assets/diagram-Z3DM3KII-CRWmdH0s.js.gz +0 -0
  94. package/dist/client/assets/{ebnfDiagram-PWID7BFC-DpAG2pFv.js → ebnfDiagram-PWID7BFC-CwItyAmD.js} +1 -1
  95. package/dist/client/assets/ebnfDiagram-PWID7BFC-CwItyAmD.js.br +0 -0
  96. package/dist/client/assets/ebnfDiagram-PWID7BFC-CwItyAmD.js.gz +0 -0
  97. package/dist/client/assets/{erDiagram-RLTQ6QDP-x4B0lUNv.js → erDiagram-RLTQ6QDP-BWF0eELE.js} +1 -1
  98. package/dist/client/assets/erDiagram-RLTQ6QDP-BWF0eELE.js.br +0 -0
  99. package/dist/client/assets/erDiagram-RLTQ6QDP-BWF0eELE.js.gz +0 -0
  100. package/dist/client/assets/{flowDiagram-HODETNUW-Dx34Jzbo.js → flowDiagram-HODETNUW-Dz_4CTgZ.js} +1 -1
  101. package/dist/client/assets/flowDiagram-HODETNUW-Dz_4CTgZ.js.br +0 -0
  102. package/dist/client/assets/flowDiagram-HODETNUW-Dz_4CTgZ.js.gz +0 -0
  103. package/dist/client/assets/{ganttDiagram-EL5Y4UJY-C7fWuwM6.js → ganttDiagram-EL5Y4UJY-V27-kSI4.js} +1 -1
  104. package/dist/client/assets/ganttDiagram-EL5Y4UJY-V27-kSI4.js.br +0 -0
  105. package/dist/client/assets/ganttDiagram-EL5Y4UJY-V27-kSI4.js.gz +0 -0
  106. package/dist/client/assets/{gestureArbiter-CwYdj3ki.js → gestureArbiter-RzRq7gG2.js} +1 -1
  107. package/dist/client/assets/gestureArbiter-RzRq7gG2.js.br +0 -0
  108. package/dist/client/assets/gestureArbiter-RzRq7gG2.js.gz +0 -0
  109. package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-B6QlpyGY.js → gitGraphDiagram-WWUBYQGX-C9WTgqiD.js} +1 -1
  110. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C9WTgqiD.js.br +0 -0
  111. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-C9WTgqiD.js.gz +0 -0
  112. package/dist/client/assets/{infoDiagram-27XIBGKW-D-cyvLtB.js → infoDiagram-27XIBGKW-DKp6kmDi.js} +1 -1
  113. package/dist/client/assets/{ishikawaDiagram-5VMMS53U-Bji-6SyT.js → ishikawaDiagram-5VMMS53U-B0UG9H7W.js} +1 -1
  114. package/dist/client/assets/ishikawaDiagram-5VMMS53U-B0UG9H7W.js.br +0 -0
  115. package/dist/client/assets/ishikawaDiagram-5VMMS53U-B0UG9H7W.js.gz +0 -0
  116. package/dist/client/assets/{journeyDiagram-3NMN7TZE-FTfWDJ8K.js → journeyDiagram-3NMN7TZE-Cgj9wfeA.js} +1 -1
  117. package/dist/client/assets/journeyDiagram-3NMN7TZE-Cgj9wfeA.js.br +0 -0
  118. package/dist/client/assets/journeyDiagram-3NMN7TZE-Cgj9wfeA.js.gz +0 -0
  119. package/dist/client/assets/{kanban-definition-UXKFOSKX-CvrvLpNJ.js → kanban-definition-UXKFOSKX-B0Su1S1B.js} +1 -1
  120. package/dist/client/assets/kanban-definition-UXKFOSKX-B0Su1S1B.js.br +0 -0
  121. package/dist/client/assets/kanban-definition-UXKFOSKX-B0Su1S1B.js.gz +0 -0
  122. package/dist/client/assets/{layout-D04TPU52.js → layout-Cte2rCfs.js} +1 -1
  123. package/dist/client/assets/layout-Cte2rCfs.js.br +0 -0
  124. package/dist/client/assets/layout-Cte2rCfs.js.gz +0 -0
  125. package/dist/client/assets/{linear-tmc9NdkO.js → linear-CC59HR-0.js} +1 -1
  126. package/dist/client/assets/linear-CC59HR-0.js.br +0 -0
  127. package/dist/client/assets/linear-CC59HR-0.js.gz +0 -0
  128. package/dist/client/assets/{mermaid.core-BghOeJM9.js → mermaid.core-DNvgq7UV.js} +5 -5
  129. package/dist/client/assets/mermaid.core-DNvgq7UV.js.br +0 -0
  130. package/dist/client/assets/mermaid.core-DNvgq7UV.js.gz +0 -0
  131. package/dist/client/assets/{mindmap-definition-YA3MSWOX-BEhTGgsF.js → mindmap-definition-YA3MSWOX-CL-Too_c.js} +1 -1
  132. package/dist/client/assets/mindmap-definition-YA3MSWOX-CL-Too_c.js.br +0 -0
  133. package/dist/client/assets/mindmap-definition-YA3MSWOX-CL-Too_c.js.gz +0 -0
  134. package/dist/client/assets/{passwordBootstrap-wvzKljMS.js → passwordBootstrap-DHkPEJte.js} +1 -1
  135. package/dist/client/assets/passwordBootstrap-DHkPEJte.js.br +0 -0
  136. package/dist/client/assets/passwordBootstrap-DHkPEJte.js.gz +0 -0
  137. package/dist/client/assets/{pegDiagram-XKGWAZYB-BX1V7YDa.js → pegDiagram-XKGWAZYB-DqHkIEjl.js} +1 -1
  138. package/dist/client/assets/pegDiagram-XKGWAZYB-DqHkIEjl.js.br +0 -0
  139. package/dist/client/assets/pegDiagram-XKGWAZYB-DqHkIEjl.js.gz +0 -0
  140. package/dist/client/assets/{pieDiagram-E7YTZNPT-D5LMv0Qe.js → pieDiagram-E7YTZNPT-6G1ciW1i.js} +1 -1
  141. package/dist/client/assets/pieDiagram-E7YTZNPT-6G1ciW1i.js.br +0 -0
  142. package/dist/client/assets/pieDiagram-E7YTZNPT-6G1ciW1i.js.gz +0 -0
  143. package/dist/client/assets/{quadrantDiagram-AXDQQJYC-4fzegqtV.js → quadrantDiagram-AXDQQJYC-C08ycYeq.js} +1 -1
  144. package/dist/client/assets/quadrantDiagram-AXDQQJYC-C08ycYeq.js.br +0 -0
  145. package/dist/client/assets/quadrantDiagram-AXDQQJYC-C08ycYeq.js.gz +0 -0
  146. package/dist/client/assets/{railroadDiagram-O6MQD6OU-bgwdyvOY.js → railroadDiagram-O6MQD6OU-BsmprHau.js} +1 -1
  147. package/dist/client/assets/railroadDiagram-O6MQD6OU-BsmprHau.js.br +0 -0
  148. package/dist/client/assets/railroadDiagram-O6MQD6OU-BsmprHau.js.gz +0 -0
  149. package/dist/client/assets/{referenceLongPress-Uw7_VECn.js → referenceLongPress-D5zkcISp.js} +1 -1
  150. package/dist/client/assets/referenceLongPress-D5zkcISp.js.br +0 -0
  151. package/dist/client/assets/referenceLongPress-D5zkcISp.js.gz +0 -0
  152. package/dist/client/assets/{requirementDiagram-BXWQKSXE-CZhlA-YX.js → requirementDiagram-BXWQKSXE-Ct7OZ0sX.js} +1 -1
  153. package/dist/client/assets/{requirementDiagram-BXWQKSXE-CZhlA-YX.js.br → requirementDiagram-BXWQKSXE-Ct7OZ0sX.js.br} +0 -0
  154. package/dist/client/assets/requirementDiagram-BXWQKSXE-Ct7OZ0sX.js.gz +0 -0
  155. package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BSLsSYgS.js → sankeyDiagram-P5KCCOFB-BcGS-zY5.js} +1 -1
  156. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BcGS-zY5.js.br +0 -0
  157. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BcGS-zY5.js.gz +0 -0
  158. package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D5q4HIce.js → sequenceDiagram-WJ2MYXX4-CzGOOMf7.js} +1 -1
  159. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CzGOOMf7.js.br +0 -0
  160. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CzGOOMf7.js.gz +0 -0
  161. package/dist/client/assets/{sizeCapture-INFHLROL-C4i6fklz.js → sizeCapture-INFHLROL-B4nglf-i.js} +1 -1
  162. package/dist/client/assets/sizeCapture-INFHLROL-B4nglf-i.js.br +0 -0
  163. package/dist/client/assets/sizeCapture-INFHLROL-B4nglf-i.js.gz +0 -0
  164. package/dist/client/assets/{sparkles-BKHm6SJH.js → sparkles-D2dbUVEe.js} +1 -1
  165. package/dist/client/assets/{stateDiagram-D77RDMKH-C7ooQSEd.js → stateDiagram-D77RDMKH-DTid36j9.js} +1 -1
  166. package/dist/client/assets/stateDiagram-D77RDMKH-DTid36j9.js.br +0 -0
  167. package/dist/client/assets/stateDiagram-D77RDMKH-DTid36j9.js.gz +0 -0
  168. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CivC-WBT.js +1 -0
  169. package/dist/client/assets/{swimlanes-42K2YHIH-BvYBqq7o.js → swimlanes-42K2YHIH-CfBzltgL.js} +1 -1
  170. package/dist/client/assets/swimlanes-42K2YHIH-CfBzltgL.js.br +0 -0
  171. package/dist/client/assets/swimlanes-42K2YHIH-CfBzltgL.js.gz +0 -0
  172. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DNedYJ8P.js +8 -0
  173. package/dist/client/assets/{timeline-definition-24CTP7MA-Chgyw7xv.js → timeline-definition-24CTP7MA-ChGIsqiN.js} +1 -1
  174. package/dist/client/assets/timeline-definition-24CTP7MA-ChGIsqiN.js.br +0 -0
  175. package/dist/client/assets/timeline-definition-24CTP7MA-ChGIsqiN.js.gz +0 -0
  176. package/dist/client/assets/{triangle-alert-CcBfd16t.js → triangle-alert-BVxZtbSu.js} +1 -1
  177. package/dist/client/assets/{vennDiagram-4TSXK5OY-CF1lMDLm.js → vennDiagram-4TSXK5OY-KovgHKV7.js} +1 -1
  178. package/dist/client/assets/vennDiagram-4TSXK5OY-KovgHKV7.js.br +0 -0
  179. package/dist/client/assets/vennDiagram-4TSXK5OY-KovgHKV7.js.gz +0 -0
  180. package/dist/client/assets/{wardleyDiagram-VM6X3IG4-CxUB2Gwu.js → wardleyDiagram-VM6X3IG4-Br0xO1Ao.js} +1 -1
  181. package/dist/client/assets/wardleyDiagram-VM6X3IG4-Br0xO1Ao.js.br +0 -0
  182. package/dist/client/assets/wardleyDiagram-VM6X3IG4-Br0xO1Ao.js.gz +0 -0
  183. package/dist/client/assets/{xychartDiagram-S5SC5T6Z-D1FK5tqF.js → xychartDiagram-S5SC5T6Z-BzbDfcqJ.js} +1 -1
  184. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BzbDfcqJ.js.br +0 -0
  185. package/dist/client/assets/xychartDiagram-S5SC5T6Z-BzbDfcqJ.js.gz +0 -0
  186. package/dist/client/index.html +1 -1
  187. package/dist/client/index.html.br +0 -0
  188. package/dist/client/index.html.gz +0 -0
  189. package/dist/client/sw.js +1 -1
  190. package/dist/client/sw.js.br +0 -0
  191. package/dist/client/sw.js.gz +0 -0
  192. package/dist/server/agent/collaborationCli.js +113 -33
  193. package/dist/server/agent/collaborationPeerTransport.js +441 -0
  194. package/dist/server/agent/collaborationPrompt.js +10 -4
  195. package/dist/server/agent/collaborationRoutes.js +75 -15
  196. package/dist/server/agent/collaborationStore.js +108 -23
  197. package/dist/server/agent/collaborationText.js +5 -0
  198. package/dist/server/agent/collaborationTmuxDelivery.js +9 -6
  199. package/dist/server/entry.js +42 -2
  200. package/dist/server/federation/runtime.js +7 -2
  201. package/dist/server/routes/terminal.js +48 -20
  202. package/package.json +1 -1
  203. package/runtime-manifest.json +3 -3
  204. package/dist/client/assets/App-Bl-MnHlK.js.br +0 -0
  205. package/dist/client/assets/App-Bl-MnHlK.js.gz +0 -0
  206. package/dist/client/assets/ChangeWalkthroughPanel-oVCcuJL3.js.br +0 -0
  207. package/dist/client/assets/ChangeWalkthroughPanel-oVCcuJL3.js.gz +0 -0
  208. package/dist/client/assets/DagPlayground-B33ZJSNC.js.br +0 -0
  209. package/dist/client/assets/DagPlayground-B33ZJSNC.js.gz +0 -0
  210. package/dist/client/assets/DiffLab-CZRP_SqH.js.br +0 -0
  211. package/dist/client/assets/DiffLab-CZRP_SqH.js.gz +0 -0
  212. package/dist/client/assets/DiffReview-BheUk7E5.js.br +0 -0
  213. package/dist/client/assets/DiffReview-BheUk7E5.js.gz +0 -0
  214. package/dist/client/assets/DiffReviewLab-DupHSbbd.js.br +0 -0
  215. package/dist/client/assets/DiffReviewLab-DupHSbbd.js.gz +0 -0
  216. package/dist/client/assets/DiffViewer-Cd5EE8E9.js.br +0 -0
  217. package/dist/client/assets/DiffViewer-Cd5EE8E9.js.gz +0 -0
  218. package/dist/client/assets/ModelPreview-Fnuxnt8A.js.br +0 -0
  219. package/dist/client/assets/ModelPreview-Fnuxnt8A.js.gz +0 -0
  220. package/dist/client/assets/RightSidebar-DTTmdNhf.js.br +0 -0
  221. package/dist/client/assets/RightSidebar-DTTmdNhf.js.gz +0 -0
  222. package/dist/client/assets/abnfDiagram-VCTEODGH-1oyW4gyC.js.br +0 -0
  223. package/dist/client/assets/abnfDiagram-VCTEODGH-1oyW4gyC.js.gz +0 -0
  224. package/dist/client/assets/app-CGZYqW3s.js +0 -35
  225. package/dist/client/assets/app-CGZYqW3s.js.br +0 -0
  226. package/dist/client/assets/app-CGZYqW3s.js.gz +0 -0
  227. package/dist/client/assets/arc-e9P469h4.js.br +0 -0
  228. package/dist/client/assets/arc-e9P469h4.js.gz +0 -0
  229. package/dist/client/assets/architectureDiagram-5GKGNRK7-CloNCKLm.js.br +0 -0
  230. package/dist/client/assets/architectureDiagram-5GKGNRK7-CloNCKLm.js.gz +0 -0
  231. package/dist/client/assets/blockDiagram-I7D4REHJ-D3wH4hOJ.js.br +0 -0
  232. package/dist/client/assets/blockDiagram-I7D4REHJ-D3wH4hOJ.js.gz +0 -0
  233. package/dist/client/assets/c4Diagram-7LVT6UL2-CX5tSHre.js.gz +0 -0
  234. package/dist/client/assets/channel-4mkr49cg.js +0 -1
  235. package/dist/client/assets/chunk-F27PBJKO-BRYx-Dtz.js.br +0 -0
  236. package/dist/client/assets/chunk-F27PBJKO-BRYx-Dtz.js.gz +0 -0
  237. package/dist/client/assets/chunk-IMKFNOWR-z6lXRY7P.js.br +0 -0
  238. package/dist/client/assets/chunk-IMKFNOWR-z6lXRY7P.js.gz +0 -0
  239. package/dist/client/assets/chunk-SVP7TREG-Bk-X0NaV.js.br +0 -0
  240. package/dist/client/assets/chunk-SVP7TREG-Bk-X0NaV.js.gz +0 -0
  241. package/dist/client/assets/chunk-TICWLB2K-BjUqsphB.js.br +0 -0
  242. package/dist/client/assets/chunk-TICWLB2K-BjUqsphB.js.gz +0 -0
  243. package/dist/client/assets/classDiagram-ZZMXUADV-DxYPnGqp.js +0 -1
  244. package/dist/client/assets/classDiagram-v2-VYDZK3BY-DxYPnGqp.js +0 -1
  245. package/dist/client/assets/cose-bilkent-JH36ORCC-Bw8F9aUq.js.br +0 -0
  246. package/dist/client/assets/cose-bilkent-JH36ORCC-Bw8F9aUq.js.gz +0 -0
  247. package/dist/client/assets/cynefin-OW5HDTMX-C5oB4ZTA.js.br +0 -0
  248. package/dist/client/assets/cynefin-OW5HDTMX-C5oB4ZTA.js.gz +0 -0
  249. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BkbwVdAD.js.br +0 -0
  250. package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BkbwVdAD.js.gz +0 -0
  251. package/dist/client/assets/dagre-GXQ25YYZ-8Jt1so7H.js.br +0 -0
  252. package/dist/client/assets/dagre-GXQ25YYZ-8Jt1so7H.js.gz +0 -0
  253. package/dist/client/assets/diagram-S7CK7UJ4-DyWUtoUO.js.br +0 -0
  254. package/dist/client/assets/diagram-S7CK7UJ4-DyWUtoUO.js.gz +0 -0
  255. package/dist/client/assets/diagram-UQ7AKVKN-BBnqsz2l.js.br +0 -0
  256. package/dist/client/assets/diagram-UQ7AKVKN-BBnqsz2l.js.gz +0 -0
  257. package/dist/client/assets/diagram-VSXAHHWV-DQ2JxOjW.js.br +0 -0
  258. package/dist/client/assets/diagram-VSXAHHWV-DQ2JxOjW.js.gz +0 -0
  259. package/dist/client/assets/diagram-VX7I27RA-Das3jLN9.js.br +0 -0
  260. package/dist/client/assets/diagram-VX7I27RA-Das3jLN9.js.gz +0 -0
  261. package/dist/client/assets/diagram-Z3DM3KII-0gbBMfrq.js.br +0 -0
  262. package/dist/client/assets/diagram-Z3DM3KII-0gbBMfrq.js.gz +0 -0
  263. package/dist/client/assets/ebnfDiagram-PWID7BFC-DpAG2pFv.js.br +0 -0
  264. package/dist/client/assets/ebnfDiagram-PWID7BFC-DpAG2pFv.js.gz +0 -0
  265. package/dist/client/assets/erDiagram-RLTQ6QDP-x4B0lUNv.js.br +0 -0
  266. package/dist/client/assets/erDiagram-RLTQ6QDP-x4B0lUNv.js.gz +0 -0
  267. package/dist/client/assets/flowDiagram-HODETNUW-Dx34Jzbo.js.br +0 -0
  268. package/dist/client/assets/flowDiagram-HODETNUW-Dx34Jzbo.js.gz +0 -0
  269. package/dist/client/assets/ganttDiagram-EL5Y4UJY-C7fWuwM6.js.br +0 -0
  270. package/dist/client/assets/ganttDiagram-EL5Y4UJY-C7fWuwM6.js.gz +0 -0
  271. package/dist/client/assets/gestureArbiter-CwYdj3ki.js.br +0 -0
  272. package/dist/client/assets/gestureArbiter-CwYdj3ki.js.gz +0 -0
  273. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6QlpyGY.js.br +0 -0
  274. package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6QlpyGY.js.gz +0 -0
  275. package/dist/client/assets/ishikawaDiagram-5VMMS53U-Bji-6SyT.js.br +0 -0
  276. package/dist/client/assets/ishikawaDiagram-5VMMS53U-Bji-6SyT.js.gz +0 -0
  277. package/dist/client/assets/journeyDiagram-3NMN7TZE-FTfWDJ8K.js.br +0 -0
  278. package/dist/client/assets/journeyDiagram-3NMN7TZE-FTfWDJ8K.js.gz +0 -0
  279. package/dist/client/assets/kanban-definition-UXKFOSKX-CvrvLpNJ.js.br +0 -0
  280. package/dist/client/assets/kanban-definition-UXKFOSKX-CvrvLpNJ.js.gz +0 -0
  281. package/dist/client/assets/layout-D04TPU52.js.br +0 -0
  282. package/dist/client/assets/layout-D04TPU52.js.gz +0 -0
  283. package/dist/client/assets/linear-tmc9NdkO.js.br +0 -0
  284. package/dist/client/assets/linear-tmc9NdkO.js.gz +0 -0
  285. package/dist/client/assets/mermaid.core-BghOeJM9.js.br +0 -0
  286. package/dist/client/assets/mermaid.core-BghOeJM9.js.gz +0 -0
  287. package/dist/client/assets/mindmap-definition-YA3MSWOX-BEhTGgsF.js.br +0 -0
  288. package/dist/client/assets/mindmap-definition-YA3MSWOX-BEhTGgsF.js.gz +0 -0
  289. package/dist/client/assets/passwordBootstrap-wvzKljMS.js.br +0 -0
  290. package/dist/client/assets/passwordBootstrap-wvzKljMS.js.gz +0 -0
  291. package/dist/client/assets/pegDiagram-XKGWAZYB-BX1V7YDa.js.br +0 -0
  292. package/dist/client/assets/pegDiagram-XKGWAZYB-BX1V7YDa.js.gz +0 -0
  293. package/dist/client/assets/pieDiagram-E7YTZNPT-D5LMv0Qe.js.br +0 -0
  294. package/dist/client/assets/pieDiagram-E7YTZNPT-D5LMv0Qe.js.gz +0 -0
  295. package/dist/client/assets/quadrantDiagram-AXDQQJYC-4fzegqtV.js.br +0 -0
  296. package/dist/client/assets/quadrantDiagram-AXDQQJYC-4fzegqtV.js.gz +0 -0
  297. package/dist/client/assets/railroadDiagram-O6MQD6OU-bgwdyvOY.js.br +0 -0
  298. package/dist/client/assets/railroadDiagram-O6MQD6OU-bgwdyvOY.js.gz +0 -0
  299. package/dist/client/assets/referenceLongPress-Uw7_VECn.js.br +0 -0
  300. package/dist/client/assets/referenceLongPress-Uw7_VECn.js.gz +0 -0
  301. package/dist/client/assets/requirementDiagram-BXWQKSXE-CZhlA-YX.js.gz +0 -0
  302. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BSLsSYgS.js.br +0 -0
  303. package/dist/client/assets/sankeyDiagram-P5KCCOFB-BSLsSYgS.js.gz +0 -0
  304. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D5q4HIce.js.br +0 -0
  305. package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D5q4HIce.js.gz +0 -0
  306. package/dist/client/assets/sizeCapture-INFHLROL-C4i6fklz.js.br +0 -0
  307. package/dist/client/assets/sizeCapture-INFHLROL-C4i6fklz.js.gz +0 -0
  308. package/dist/client/assets/stateDiagram-D77RDMKH-C7ooQSEd.js.br +0 -0
  309. package/dist/client/assets/stateDiagram-D77RDMKH-C7ooQSEd.js.gz +0 -0
  310. package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CYPjXGda.js +0 -1
  311. package/dist/client/assets/swimlanes-42K2YHIH-BvYBqq7o.js.br +0 -0
  312. package/dist/client/assets/swimlanes-42K2YHIH-BvYBqq7o.js.gz +0 -0
  313. package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DGury37v.js +0 -8
  314. package/dist/client/assets/timeline-definition-24CTP7MA-Chgyw7xv.js.br +0 -0
  315. package/dist/client/assets/timeline-definition-24CTP7MA-Chgyw7xv.js.gz +0 -0
  316. package/dist/client/assets/vennDiagram-4TSXK5OY-CF1lMDLm.js.br +0 -0
  317. package/dist/client/assets/vennDiagram-4TSXK5OY-CF1lMDLm.js.gz +0 -0
  318. package/dist/client/assets/wardleyDiagram-VM6X3IG4-CxUB2Gwu.js.br +0 -0
  319. package/dist/client/assets/wardleyDiagram-VM6X3IG4-CxUB2Gwu.js.gz +0 -0
  320. package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1FK5tqF.js.br +0 -0
  321. package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1FK5tqF.js.gz +0 -0
@@ -0,0 +1,441 @@
1
+ import { encryptedRequestSubject } from '../federation/requestContext.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
4
+ import { dirname } from 'node:path';
5
+ import WebSocket from 'ws';
6
+ import { AsyncQueue, toBase64, fromBase64, PacketChannel } from '../federation/packets.js';
7
+ import { secureConnection } from '../federation/secureProtocol.js';
8
+ import { socketDuplex } from '../federation/socketDuplex.js';
9
+ import { readPinnedCertificateAuthority } from '../federation/trustedCa.js';
10
+ import { COLLAB_LIMITS, STATUS_RANK } from './collaborationProtocol.js';
11
+ import { terminalMessage } from './collaborationStore.js';
12
+ /** The outer API gate already authenticated local CLI tokens. Browser callers
13
+ * additionally need durable administration authority; open-access sessions may
14
+ * not turn temporary access into persistent peer authorization. */
15
+ export function assertPeerRegistrationAuthority(req) {
16
+ const subjectId = encryptedRequestSubject(req);
17
+ if (!subjectId)
18
+ return;
19
+ const runtime = req.app.locals.passwordRuntime;
20
+ if (!runtime?.store.authorize({ subjectId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed)
21
+ throw new Error('AUTHORIZATION_DENIED');
22
+ }
23
+ export function remoteSession(origin, id) { return `remote:${encodeURIComponent(origin)}:${encodeURIComponent(id)}`; }
24
+ function localSession(origin, id) {
25
+ const prefix = `remote:${encodeURIComponent(origin)}:`;
26
+ return id.startsWith(prefix) ? decodeURIComponent(id.slice(prefix.length)) : id;
27
+ }
28
+ export function validateCollaborationNode(input) {
29
+ const url = new URL(input.origin);
30
+ if (url.protocol !== 'https:' || url.origin !== input.origin || url.username || url.password
31
+ || !/^12D3KooW[a-zA-Z0-9]{30,60}$/.test(input.serviceId)
32
+ || (input.caFingerprint256 !== undefined && !/^(?:[\da-f]{2}:){31}[\da-f]{2}$/i.test(input.caFingerprint256)))
33
+ throw new Error('INVALID_COLLABORATION_NODE');
34
+ }
35
+ function mapContext(context, map) {
36
+ return { instructions: context.instructions ? { ...context.instructions, updatedBy: map(context.instructions.updatedBy) } : undefined,
37
+ roles: context.roles ? Object.fromEntries(Object.entries(context.roles).map(([id, role]) => [map(id), role])) : undefined,
38
+ roleVersions: context.roleVersions ? Object.fromEntries(Object.entries(context.roleVersions).map(([id, revision]) => [map(id), revision])) : undefined };
39
+ }
40
+ /** Small RPC client over the same authenticated Noise transport as the page.
41
+ * No cookies, browser device keys, or direct HTTP business requests. */
42
+ export class CollaborationRpc {
43
+ channel;
44
+ pending = new Map();
45
+ closed = false;
46
+ constructor(channel) {
47
+ this.channel = channel;
48
+ void this.read();
49
+ void channel.done.catch(error => this.close(error));
50
+ }
51
+ async read() {
52
+ try {
53
+ for await (const packet of this.channel.read()) {
54
+ const request = this.pending.get(packet.id);
55
+ if (!request)
56
+ continue;
57
+ clearTimeout(request.timer);
58
+ this.pending.delete(packet.id);
59
+ if (packet.type === 'error')
60
+ request.reject(new Error(String(packet.error)));
61
+ else
62
+ request.resolve(packet);
63
+ }
64
+ }
65
+ catch (error) {
66
+ this.close(error instanceof Error ? error : new Error('PEER_DISCONNECTED'));
67
+ }
68
+ finally {
69
+ this.close();
70
+ }
71
+ }
72
+ request(packet) {
73
+ if (this.closed)
74
+ return Promise.reject(new Error('PEER_DISCONNECTED'));
75
+ if (this.pending.size >= 8)
76
+ return Promise.reject(new Error('PEER_BUSY'));
77
+ return new Promise((resolve, reject) => {
78
+ const id = randomUUID();
79
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('PEER_TIMEOUT')); this.close(); }, 10_000);
80
+ this.pending.set(id, { resolve, reject, timer });
81
+ try {
82
+ this.channel.send({ ...packet, id });
83
+ }
84
+ catch (error) {
85
+ this.close(error instanceof Error ? error : new Error('PEER_DISCONNECTED'));
86
+ }
87
+ });
88
+ }
89
+ close(error = new Error('PEER_DISCONNECTED')) {
90
+ if (this.closed)
91
+ return;
92
+ this.closed = true;
93
+ this.channel.close();
94
+ for (const item of this.pending.values()) {
95
+ clearTimeout(item.timer);
96
+ item.reject(error);
97
+ }
98
+ this.pending.clear();
99
+ }
100
+ }
101
+ export async function connectCollaborationRpc(identity, peer, endpoint) {
102
+ validateCollaborationNode(peer);
103
+ const ca = peer.caFingerprint256 ? await readPinnedCertificateAuthority(peer.origin, peer.caFingerprint256) : undefined;
104
+ const url = endpoint ?? `${peer.origin.replace(/^https:/, 'wss:')}/api/federation/secure`;
105
+ const socket = new WebSocket(url, { ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 3000, maxPayload: 1024 * 1024 });
106
+ const relay = endpoint ? relayDuplex(socket, peer.serviceId) : undefined;
107
+ const duplex = relay?.duplex ?? socketDuplex(socket);
108
+ try {
109
+ await new Promise((resolve, reject) => {
110
+ socket.once('open', resolve);
111
+ socket.once('error', reject);
112
+ socket.once('close', () => reject(new Error('PEER_DISCONNECTED')));
113
+ });
114
+ if (relay)
115
+ await relay.ready;
116
+ const secured = await secureConnection({ identity, duplex, initiator: true, targetPinnedPeerId: peer.serviceId, signal: AbortSignal.timeout(5000) });
117
+ return new CollaborationRpc(new PacketChannel(secured.duplex));
118
+ }
119
+ catch (error) {
120
+ socket.terminate();
121
+ throw error;
122
+ }
123
+ }
124
+ function relayDuplex(socket, target) {
125
+ const queue = new AsyncQueue(64), streamId = randomUUID();
126
+ let readyResolve, readyReject;
127
+ const ready = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
128
+ void ready.catch(() => { });
129
+ const timer = setTimeout(() => fail(new Error('ROUTE_TIMEOUT')), 5000);
130
+ const fail = (error) => { clearTimeout(timer); readyReject(error); queue.end(error); socket.close(); };
131
+ const send = (frame) => {
132
+ if (socket.readyState !== 1 || socket.bufferedAmount > 1024 * 1024)
133
+ throw new Error('ROUTE_CONGESTED');
134
+ socket.send(JSON.stringify(frame));
135
+ };
136
+ socket.on('error', fail);
137
+ socket.on('close', () => fail(new Error('ROUTE_CLOSED')));
138
+ socket.on('message', (raw, binary) => {
139
+ try {
140
+ if (binary || raw.toString().length > 256 * 1024)
141
+ throw new Error('INVALID_RELAY_FRAME');
142
+ const frame = JSON.parse(raw.toString());
143
+ if (frame.type === 'ready') {
144
+ send({ type: 'open', streamId, serviceId: target });
145
+ return;
146
+ }
147
+ if (frame.streamId !== streamId)
148
+ return;
149
+ if (frame.type === 'opened') {
150
+ clearTimeout(timer);
151
+ readyResolve();
152
+ return;
153
+ }
154
+ if (frame.type !== 'data' || frame.encoding !== 'base64' || typeof frame.payload !== 'string')
155
+ throw new Error('ROUTE_CLOSED');
156
+ queue.push(fromBase64(frame.payload));
157
+ }
158
+ catch (error) {
159
+ fail(error instanceof Error ? error : new Error('INVALID_RELAY_FRAME'));
160
+ }
161
+ });
162
+ return { ready, duplex: { source: queue, close: () => socket.close(), async sink(source) {
163
+ await ready;
164
+ for await (const bytes of source)
165
+ for (let offset = 0; offset < bytes.length; offset += 65536) {
166
+ while (socket.bufferedAmount > 256 * 1024) {
167
+ if (socket.readyState !== 1)
168
+ throw new Error('ROUTE_CLOSED');
169
+ await new Promise(resolve => setTimeout(resolve, 10));
170
+ }
171
+ send({ type: 'data', streamId, encoding: 'base64', payload: toBase64(bytes.subarray(offset, offset + 65536)) });
172
+ }
173
+ } } };
174
+ }
175
+ /** Group-scoped, durable server-to-server delivery. A binding is installed only
176
+ * by an already authorized group administrator, never by the remote peer itself. */
177
+ export class CollaborationPeerTransport {
178
+ options;
179
+ bindings = [];
180
+ clients = new Map();
181
+ running = new Set();
182
+ accepted = new Set();
183
+ shellConfirmed = new Set();
184
+ nextAttempt = new Map();
185
+ timer;
186
+ stopped = false;
187
+ constructor(options) {
188
+ this.options = options;
189
+ try {
190
+ const data = JSON.parse(readFileSync(options.file, 'utf8'));
191
+ if (data.version === 1 && Array.isArray(data.bindings))
192
+ this.bindings = data.bindings.filter((b) => {
193
+ try {
194
+ validateCollaborationNode(b.peer);
195
+ return typeof b.groupId === 'string' && new URL(b.localOrigin).origin === b.localOrigin;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ });
201
+ }
202
+ catch { /* First run has no server authorizations. */ }
203
+ }
204
+ notify() {
205
+ for (const b of this.bindings) {
206
+ const key = `${b.groupId}:${b.peer.serviceId}`;
207
+ if (!this.options.store.federationSnapshot().messages.some(m => m.groupId === b.groupId && m.status === 'pending'
208
+ && this.options.store.diagnostic(m.id)?.last_error))
209
+ this.nextAttempt.delete(key);
210
+ }
211
+ // A queued message may arrive while an empty scan is finishing.
212
+ setImmediate(() => this.wake());
213
+ }
214
+ start() { this.options.store.onMessageQueued = () => this.notify(); this.timer = setInterval(() => this.wake(), 500); this.timer.unref(); this.wake(); }
215
+ close() { this.stopped = true; this.options.store.onMessageQueued = undefined; clearInterval(this.timer); for (const client of this.clients.values())
216
+ void client.then(c => c.close()).catch(() => { }); this.clients.clear(); }
217
+ live(b) {
218
+ const group = this.options.store.getGroup(b.groupId);
219
+ return !!group && !group.deleted && group.federated && group.sessionIds.some(id => id.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`));
220
+ }
221
+ configure(groupId, localOrigin, nodes) {
222
+ if (!Array.isArray(nodes) || nodes.length > 64)
223
+ throw new Error('INVALID_COLLABORATION_NODES');
224
+ const group = this.options.store.getGroup(groupId);
225
+ if (!group || group.deleted || !group.federated)
226
+ throw new Error('GROUP_NOT_FOUND');
227
+ const local = nodes.find(n => n.serviceId === this.options.serviceId);
228
+ if (!local || local.origin !== localOrigin)
229
+ throw new Error('LOCAL_NODE_MISMATCH');
230
+ for (const node of nodes)
231
+ validateCollaborationNode(node);
232
+ const peers = nodes.filter(n => n.serviceId !== this.options.serviceId && group.sessionIds.some(id => id.startsWith(`remote:${encodeURIComponent(n.origin)}:`)));
233
+ const next = [...this.bindings.filter(b => b.groupId !== groupId), ...peers.map(peer => ({ groupId, localOrigin, peer }))];
234
+ if (JSON.stringify(next) !== JSON.stringify(this.bindings)) {
235
+ mkdirSync(dirname(this.options.file), { recursive: true, mode: 0o700 });
236
+ const temp = `${this.options.file}.${process.pid}.tmp`;
237
+ writeFileSync(temp, JSON.stringify({ version: 1, bindings: next }), { mode: 0o600 });
238
+ renameSync(temp, this.options.file);
239
+ this.bindings = next;
240
+ this.nextAttempt.clear();
241
+ for (const client of this.clients.values())
242
+ void client.then(c => c.close()).catch(() => { });
243
+ this.clients.clear();
244
+ }
245
+ this.notify();
246
+ }
247
+ canRoute(subjectId, targetId) {
248
+ return this.bindings.some(b => b.peer.serviceId === subjectId && this.live(b)
249
+ && this.bindings.some(target => target.groupId === b.groupId && target.peer.serviceId === targetId && this.live(target)));
250
+ }
251
+ binding(subjectId, groupId) {
252
+ const binding = this.bindings.find(b => b.peer.serviceId === subjectId && b.groupId === groupId && this.live(b));
253
+ if (!binding)
254
+ throw new Error('COLLABORATION_NOT_AUTHORIZED');
255
+ return binding;
256
+ }
257
+ /** Remote peers can submit only their own members' messages and query receipts
258
+ * for those messages. They cannot create groups, rewrite bodies or administer sessions. */
259
+ receive(subjectId, packet) {
260
+ const b = this.binding(subjectId, String(packet.groupId));
261
+ const store = this.options.store;
262
+ if (packet.context)
263
+ store.mergeContext(b.groupId, mapContext(packet.context, id => localSession(b.localOrigin, id)));
264
+ if (packet.message) {
265
+ const message = packet.message;
266
+ if (message.groupId !== b.groupId || typeof message.fromSessionId !== 'string' || message.fromSessionId.startsWith('remote:')
267
+ || typeof message.toSessionId !== 'string' || !message.toSessionId.startsWith(`remote:${encodeURIComponent(b.localOrigin)}:`)
268
+ || Buffer.byteLength(JSON.stringify(message)) > COLLAB_LIMITS.wire_bytes)
269
+ throw new Error('INVALID_PEER_MESSAGE');
270
+ const incoming = { ...message, fromSessionId: remoteSession(b.peer.origin, message.fromSessionId),
271
+ toSessionId: localSession(b.localOrigin, message.toSessionId), status: 'pending', deliveredAt: null, readAt: null,
272
+ snapshot: null, deliverySource: undefined, readSource: undefined, shellConfirmed: message.shellConfirmed === true };
273
+ const existing = store.getMessage(message.id);
274
+ if (existing && (existing.groupId !== incoming.groupId || existing.fromSessionId !== incoming.fromSessionId
275
+ || existing.toSessionId !== incoming.toSessionId || existing.content !== incoming.content))
276
+ throw new Error('MESSAGE_ID_CONFLICT');
277
+ if (!existing)
278
+ store.mergeFederatedMessages([incoming]);
279
+ else if (incoming.shellConfirmed && !existing.shellConfirmed)
280
+ store.mergeFederatedMessages([{ ...existing, shellConfirmed: true }]);
281
+ if (!store.getMessage(message.id))
282
+ throw new Error('PEER_REJECTED_MESSAGE');
283
+ this.options.deliver(incoming.toSessionId);
284
+ }
285
+ if (!Array.isArray(packet.ids) || packet.ids.length > 20 || packet.ids.some(id => typeof id !== 'string'))
286
+ throw new Error('INVALID_RECEIPT_IDS');
287
+ const receipts = packet.ids.flatMap(id => {
288
+ const message = store.getMessage(id);
289
+ if (!message || message.groupId !== b.groupId || !message.fromSessionId?.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`)
290
+ || message.toSessionId.startsWith('remote:'))
291
+ return [];
292
+ const facts = terminalMessage(message);
293
+ const known = Array.isArray(packet.known) ? packet.known.find((item) => item?.id === message.id) : undefined;
294
+ if (known && known.status === facts.status && known.deliveredAt === facts.deliveredAt && known.shellConfirmed === message.shellConfirmed && (known.hasSnapshot || !message.snapshot))
295
+ return [];
296
+ const receipt = store.receipt(message.id);
297
+ return [{ id: message.id, status: facts.status, deliveredAt: facts.deliveredAt,
298
+ snapshot: (receipt.snapshot ?? '').slice(0, 16000) || null, deliverySource: facts.deliverySource,
299
+ failureReason: message.failureReason, shellConfirmed: message.shellConfirmed, last_error: store.diagnostic(message.id)?.last_error ?? null }];
300
+ });
301
+ const group = store.getGroup(b.groupId);
302
+ const activity = this.options.activity?.().filter(item => group.sessionIds.includes(item.sessionId) && !item.sessionId.startsWith('remote:')) ?? [];
303
+ return { activity, context: mapContext(store.context(b.groupId), id => id.startsWith('remote:') ? id : remoteSession(b.localOrigin, id)), receipts, receivedIds: packet.ids.filter(id => {
304
+ const message = store.getMessage(id);
305
+ return message?.groupId === b.groupId && message.fromSessionId?.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`) && !message.toSessionId.startsWith('remote:');
306
+ }) };
307
+ }
308
+ wake() {
309
+ if (this.stopped)
310
+ return;
311
+ for (const b of this.bindings) {
312
+ const key = `${b.groupId}:${b.peer.serviceId}`;
313
+ if (!this.live(b) || this.running.has(key) || (this.nextAttempt.get(key) ?? 0) > Date.now())
314
+ continue;
315
+ this.running.add(key);
316
+ void this.sync(b).catch(() => { this.nextAttempt.set(key, Date.now() + 1000); }).finally(() => this.running.delete(key));
317
+ }
318
+ // No registration must be visible, rather than an unexplained zero-attempt queue.
319
+ for (const group of this.options.store.list())
320
+ for (const m of this.options.store.federationSnapshot().messages.filter(message => message.groupId === group.id)) {
321
+ if (m.status !== 'pending' || m.fromSessionId?.startsWith('remote:') || !m.toSessionId.startsWith('remote:'))
322
+ continue;
323
+ if (this.bindings.some(b => b.groupId === group.id && this.live(b) && m.toSessionId.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`)))
324
+ continue;
325
+ const prior = this.options.store.diagnostic(m.id);
326
+ if (prior?.last_error === 'PEER_REGISTRATION_REQUIRED' && Date.now() - prior.checked_at < 10_000)
327
+ continue;
328
+ this.options.store.recordTransport(m.id, { relay_online: null, peer_reachable: null, attempt_count: prior?.attempt_count ?? 0,
329
+ next_retry_at: null, last_error: 'PEER_REGISTRATION_REQUIRED', checked_at: Date.now() });
330
+ }
331
+ }
332
+ async client(b) {
333
+ const key = b.peer.serviceId;
334
+ let pending = this.clients.get(key);
335
+ if (pending) {
336
+ const client = await pending;
337
+ if (!client.closed)
338
+ return client;
339
+ this.clients.delete(key);
340
+ }
341
+ pending = (async () => {
342
+ try {
343
+ return await this.options.connect(b.peer);
344
+ }
345
+ catch (directError) {
346
+ for (const entry of this.bindings.filter(e => e.groupId === b.groupId && e.peer.serviceId !== b.peer.serviceId && this.live(e))) {
347
+ try {
348
+ return await this.options.connect(b.peer, entry.peer);
349
+ }
350
+ catch { /* Try the next explicitly registered entry. */ }
351
+ }
352
+ throw directError;
353
+ }
354
+ })();
355
+ this.clients.set(key, pending);
356
+ try {
357
+ return await pending;
358
+ }
359
+ catch (error) {
360
+ if (this.clients.get(key) === pending)
361
+ this.clients.delete(key);
362
+ throw error;
363
+ }
364
+ }
365
+ async sync(b) {
366
+ const store = this.options.store, key = `${b.groupId}:${b.peer.serviceId}`;
367
+ const messages = store.federationSnapshot().messages.filter(m => m.groupId === b.groupId).filter(m => !m.fromSessionId?.startsWith('remote:') && m.fromSessionId !== null
368
+ && m.toSessionId.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`) && !['failed', 'expired'].includes(m.status)
369
+ && (m.status === 'pending' || !m.snapshot)).sort((a, b) => Number(b.status === 'pending') - Number(a.status === 'pending'));
370
+ const pending = messages.filter(m => m.status === 'pending');
371
+ const targetErrors = new Map();
372
+ try {
373
+ const client = await this.client(b);
374
+ for (let offset = 0; offset < Math.max(1, messages.length); offset += 20) {
375
+ const batch = messages.slice(offset, offset + 20);
376
+ const requests = batch.filter(m => m.status === 'pending' && (!this.accepted.has(m.id) || m.shellConfirmed && !this.shellConfirmed.has(m.id)));
377
+ const payloads = requests.length ? requests : [undefined];
378
+ for (const message of payloads) {
379
+ const result = await client.request({ type: 'collaboration-exchange', groupId: b.groupId,
380
+ context: mapContext(store.context(b.groupId), id => id.startsWith('remote:') ? id : remoteSession(b.localOrigin, id)),
381
+ ids: batch.map(m => m.id), known: batch.map(m => ({ id: m.id, status: m.status, deliveredAt: m.deliveredAt, hasSnapshot: !!m.snapshot, shellConfirmed: m.shellConfirmed })), ...(message ? { message } : {}) });
382
+ if (Array.isArray(result.activity))
383
+ for (const item of result.activity.slice(0, 500)) {
384
+ if (!item || typeof item.sessionId !== 'string' || item.sessionId.startsWith('remote:'))
385
+ continue;
386
+ const id = remoteSession(b.peer.origin, item.sessionId);
387
+ if (store.getGroup(b.groupId)?.sessionIds.includes(id))
388
+ store.recordPeerActivity(id, item.last_terminal_output_at, item.activity_observed_at);
389
+ }
390
+ if (result.context)
391
+ store.mergeContext(b.groupId, mapContext(result.context, id => localSession(b.localOrigin, id)));
392
+ if (Array.isArray(result.receivedIds))
393
+ for (const item of batch) {
394
+ if (result.receivedIds.includes(item.id))
395
+ this.accepted.add(item.id);
396
+ else
397
+ this.accepted.delete(item.id);
398
+ }
399
+ if (this.accepted.size > 2000) {
400
+ const retained = new Set(store.federationSnapshot().messages.map(m => m.id));
401
+ for (const id of this.accepted)
402
+ if (!retained.has(id)) {
403
+ this.accepted.delete(id);
404
+ this.shellConfirmed.delete(id);
405
+ }
406
+ }
407
+ if (!Array.isArray(result.receipts))
408
+ throw new Error('INVALID_PEER_RECEIPTS');
409
+ for (const receipt of result.receipts) {
410
+ const existing = batch.find(m => m.id === receipt.id);
411
+ if (!existing)
412
+ continue;
413
+ if (receipt.shellConfirmed)
414
+ this.shellConfirmed.add(existing.id);
415
+ if (typeof receipt.last_error === 'string')
416
+ targetErrors.set(existing.id, receipt.last_error);
417
+ if (!receipt.status || !Object.hasOwn(STATUS_RANK, receipt.status)
418
+ || (receipt.deliveredAt != null && !Number.isFinite(receipt.deliveredAt)))
419
+ throw new Error('INVALID_PEER_RECEIPT');
420
+ store.mergeFederatedMessages([{ ...existing, status: receipt.status === 'read' ? 'delivered' : receipt.status, deliveredAt: receipt.deliveredAt ?? null,
421
+ readAt: null, snapshot: typeof receipt.snapshot === 'string' ? receipt.snapshot.slice(0, 16000) : null,
422
+ deliverySource: receipt.deliverySource, readSource: undefined, failureReason: receipt.failureReason }]);
423
+ }
424
+ }
425
+ }
426
+ for (const m of pending)
427
+ store.recordTransport(m.id, { relay_online: true, peer_reachable: true,
428
+ attempt_count: (store.diagnostic(m.id)?.attempt_count ?? 0) + 1, next_retry_at: m.status === 'pending' ? Date.now() + 500 : null, last_error: targetErrors.get(m.id) ?? null, checked_at: Date.now(),
429
+ ...(this.accepted.has(m.id) ? { remote_received_at: store.diagnostic(m.id)?.remote_received_at ?? Date.now() } : {}) });
430
+ this.nextAttempt.set(key, Date.now() + (messages.some(m => m.status === 'pending') ? 250 : 1000));
431
+ }
432
+ catch (error) {
433
+ const attempts = Math.max(0, ...pending.map(m => store.diagnostic(m.id)?.attempt_count ?? 0)) + 1;
434
+ const next = Date.now() + Math.min(10_000, 500 * 2 ** Math.min(attempts - 1, 5));
435
+ this.nextAttempt.set(key, next);
436
+ for (const m of pending)
437
+ store.recordTransport(m.id, { relay_online: true, peer_reachable: false, attempt_count: attempts, remote_received_at: store.diagnostic(m.id)?.remote_received_at,
438
+ next_retry_at: next, last_error: error instanceof Error ? error.message.slice(0, 300) : 'PEER_UNREACHABLE', checked_at: Date.now() });
439
+ }
440
+ }
441
+ }
@@ -3,7 +3,7 @@ import { sanitizeCollaborationRole } from './collaborationStore.js';
3
3
  const SHELL_RULE = '─'.repeat(30);
4
4
  /** Body larger than this (in UTF-8 bytes) is replaced by a retrieval pointer
5
5
  * instead of being injected into the terminal verbatim. */
6
- const MAX_INLINE_BODY_BYTES = 8_192;
6
+ const MAX_INLINE_BODY_BYTES = 4_096;
7
7
  /**
8
8
  * Characters that would corrupt the delivery shell header `「X」群(N 个成员)`
9
9
  * and the per-message `来自:X · kind` line: the quote brackets and the middle
@@ -73,6 +73,12 @@ function formatCollaborationMessage(message, source, fannedNames, fence, token)
73
73
  const lines = [`来自:${source} · ${message.kind}${fannedNames.length ? ' · 群发' : ''}`];
74
74
  if (fannedNames.length)
75
75
  lines.push(`同时发给了:${fannedNames.join('、')}`);
76
+ const rules = message.instructions;
77
+ if (rules?.text) {
78
+ lines.push(`群协作约定(版本 ${rules.version},不改变用户授权):`);
79
+ lines.push(Buffer.byteLength(rules.text) <= 1024 ? rules.text
80
+ : `执行 td collab message get ${token} --text 查看本消息附带的完整群规与正文。`);
81
+ }
76
82
  lines.push('', fence, message.content, fence);
77
83
  if (message.task)
78
84
  lines.push('', `任务上报:${JSON.stringify(message.task)}`);
@@ -92,7 +98,7 @@ export function formatCollaborationDelivery(input) {
92
98
  const token = tokens.get(message.id) ?? message.id;
93
99
  const bytes = Buffer.byteLength(message.content);
94
100
  const body = bytes > MAX_INLINE_BODY_BYTES
95
- ? `大消息已完整保存(${bytes} 字节)。使用 td collab message get ${token} --json 获取正文;不要把这条提示当作消息正文。`
101
+ ? `大消息已完整保存(${bytes} 字节)。先执行 td collab message get ${token} --text 查看完整正文;不要把这条提示当作消息正文。`
96
102
  : message.content;
97
103
  const fence = '`'.repeat(Math.max(3, ...Array.from(body.matchAll(/`+/g), (match) => match[0].length + 1)));
98
104
  const fannedNames = (message.fanOutIds ?? [])
@@ -122,13 +128,13 @@ export function formatCollaborationDelivery(input) {
122
128
  const routingHelp = input.showRoutingHelp === false ? [] : (peers.length ? ['联系其他成员:', ...peers] : []);
123
129
  const dynamicNotices = [
124
130
  ...(peerIds.some((id) => id.startsWith('remote:'))
125
- ? ['跨服务通信使用 td collab;转发客户端须保持运行,网页或 PWA 暂停后需返回前台继续转发。'] : []),
131
+ ? ['跨服务通信使用 td collab;已登记节点由服务后台直接投递,使用 message get 查看送达回执与重试原因。'] : []),
126
132
  ...unreachable.map((id) => {
127
133
  const session = sessionsById.get(id);
128
134
  return `注意:${session ? sanitizeCollaborationName(session.name) : id} 服务不可达,消息无法送达;仅可排队等待重连。`;
129
135
  }),
130
136
  ];
131
- const captureHelp = '想看伙伴正在做什么、是否卡住:先用 `td collab capture <会话ID> --text` 只读当前屏幕,不打断对方;ID 用 `td collab status --text` 查。仅同组本机 tmux;远端用 send 询问。快照不等于任务完成,按需查看,避免循环轮询。';
137
+ const captureHelp = '想看伙伴正在做什么、是否卡住:先用 `td collab capture <会话ID> --text` 只读当前屏幕;可加 --lines 200 查看历史,不打断对方;ID 用 `td collab status --text` 查。仅同组本机 tmux;远端用 send 询问。快照不等于任务完成,按需查看,避免循环轮询。';
132
138
  const notes = [...routingHelp, ...dynamicNotices, '更多操作:`td collab --help`。',
133
139
  ...(input.showRoutingHelp === false ? [] : [captureHelp])];
134
140
  // Notes (education + dynamic notices) live inside the shell, after the last
@@ -1,3 +1,5 @@
1
+ import { terminalMessage } from './collaborationStore.js';
2
+ import { assertPeerRegistrationAuthority } from './collaborationPeerTransport.js';
1
3
  import { Router } from 'express';
2
4
  import { COLLAB_LIMITS, CollaborationError, MIN_ID_PREFIX_LENGTH, ambiguousIdMessage, extrasFromBody } from './collaborationProtocol.js';
3
5
  /** Member names arrive agent-authored over a CLI; control characters would
@@ -21,6 +23,27 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
21
23
  code: error instanceof CollaborationError ? error.code : 'COLLABORATION_ERROR', error: error instanceof Error ? error.message : String(error) });
22
24
  }
23
25
  };
26
+ router.get('/transport', run((req, res) => {
27
+ if (!req.app.locals.collaborationNode)
28
+ throw new Error('COLLABORATION_UNAVAILABLE');
29
+ res.json({ ok: true, node: req.app.locals.collaborationNode });
30
+ }));
31
+ router.post('/transport', run((req, res, sessionId) => {
32
+ assertPeerRegistrationAuthority(req);
33
+ const resolved = store.resolveGroupId(String(req.body.groupId ?? ''));
34
+ if (resolved.status !== 'ok')
35
+ throw new Error('GROUP_NOT_FOUND_OR_AMBIGUOUS');
36
+ if (!store.groupsForSession(sessionId).some(group => group.id === resolved.id))
37
+ throw new Error('GROUP_NOT_FOUND');
38
+ const node = req.app.locals.collaborationNode;
39
+ if (!node || !req.app.locals.collaborationTransport || !Array.isArray(req.body.nodes))
40
+ throw new Error('COLLABORATION_UNAVAILABLE');
41
+ const local = req.body.nodes.find((item) => item?.serviceId === node.serviceId);
42
+ if (!local)
43
+ throw new Error('LOCAL_NODE_MISMATCH');
44
+ req.app.locals.collaborationTransport.configure(resolved.id, local.origin, req.body.nodes);
45
+ res.json({ ok: true, group_id: resolved.id, transport: 'server' });
46
+ }));
24
47
  router.post('/route/rebind', run(async (req, res, sessionId) => {
25
48
  if (!rebind)
26
49
  throw new CollaborationError('ROUTE_REBIND_UNAVAILABLE', 'Route rebinding is not available');
@@ -87,10 +110,10 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
87
110
  router.get('/capabilities', run((_req, res) => {
88
111
  res.json({ protocol_version: 2, limits: COLLAB_LIMITS,
89
112
  routing: { background_recovery: true, explicit_rebind: Boolean(rebind), fixed_tmux_pane: true },
90
- statuses: ['pending', 'delivered', 'read', 'failed', 'expired'], queued_status: 'pending',
91
- semantics: { delivered: 'written to terminal; no application acknowledgement implied', read: 'explicit consumer acknowledgement',
113
+ statuses: ['pending', 'delivered', 'failed', 'expired'], queued_status: 'pending',
114
+ semantics: { delivered: 'written to terminal; no application acknowledgement implied',
92
115
  ack: 'explicit response_kind=ack', result: 'explicit response_kind=result; inspect task.status and evidence',
93
- done: 'adapter reports turn ended, never proof of task completion', heartbeat: 'null unless explicitly observed',
116
+ snapshot: 'terminal observation, never proof of reading or task completion',
94
117
  timeout: 'stops waiting, does not cancel delivery',
95
118
  retry: 'same message id may be submitted again after a crash or uncertain transport result; consumers deduplicate by message id' } });
96
119
  }));
@@ -141,14 +164,13 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
141
164
  const messages = store.send({ ...extrasFromBody(req.body), groupId: original.groupId, fromSessionId: sessionId,
142
165
  toSessionIds: [original.fromSessionId], kind: 'reply', content: typeof req.body.content === 'string' ? req.body.content : '',
143
166
  replyTo: original.id, threadId: original.threadId });
144
- // Reply is explicit consumption; only mark after successful validation/persistence.
145
- store.markRead([original.id]);
146
167
  await awaitDelivery(messages[0].id, (await deliver(original.fromSessionId)) ?? null);
147
168
  sent(res, messages.map((message) => message.id));
148
169
  }));
149
170
  router.get('/message/:id', run((req, res, sessionId) => {
150
171
  const message = ownMessage(String(req.params.id), sessionId);
151
- res.json({ ok: true, ...store.receipt(message.id), ...(req.query.receipt_only === 'true' ? {} : { message }) });
172
+ const raw = req.query.raw === 'true';
173
+ res.json({ ok: true, ...store.receipt(message.id, raw), ...(req.query.receipt_only === 'true' ? {} : { message: terminalMessage(message, raw) }) });
152
174
  }));
153
175
  router.post('/message/:id/confirm-shell', run(async (req, res, sessionId) => {
154
176
  const message = ownMessage(String(req.params.id), sessionId);
@@ -159,10 +181,10 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
159
181
  await awaitDelivery(message.id, outcome);
160
182
  sent(res, [message.id]);
161
183
  }));
162
- router.post('/message/:id/read', run((req, res, sessionId) => {
163
- const message = ownMessage(String(req.params.id), sessionId, true);
164
- store.markRead([message.id]);
165
- res.json({ ok: true, ...store.receipt(message.id) });
184
+ // Old clients receive an explicit error instead of a fabricated read receipt.
185
+ router.post('/message/:id/read', run((req, _res, sessionId) => {
186
+ ownMessage(String(req.params.id), sessionId, true);
187
+ throw new CollaborationError('READ_RECEIPTS_UNSUPPORTED', 'Terminal delivery cannot observe reading; use message get for the snapshot or reply for an explicit response', 410);
166
188
  }));
167
189
  router.get('/inbox', run((req, res, sessionId) => {
168
190
  const string = (key) => typeof req.query[key] === 'string' ? req.query[key] : undefined;
@@ -185,14 +207,29 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
185
207
  if (resolved.status === 'ok')
186
208
  threadFilter = resolved.id;
187
209
  }
188
- const page = store.page(sessionId, { unread: string('unread') === 'true', since, afterId: string('after_id'), cursor: string('cursor'),
210
+ let fromFilter = string('from');
211
+ if (fromFilter) {
212
+ const allowedIds = [...new Set(store.groupsForSession(sessionId).flatMap(group => group.sessionIds))];
213
+ if (!allowedIds.includes(fromFilter)) {
214
+ const matches = fromFilter.length < 4 ? [] : allowedIds.filter(id => id.startsWith(fromFilter)
215
+ || (id.startsWith('remote:') && decodeURIComponent(id.slice(id.lastIndexOf(':') + 1)).startsWith(fromFilter)));
216
+ if (matches.length > 1)
217
+ throw new CollaborationError('SESSION_ID_AMBIGUOUS', '发送者短 ID 匹配多个成员,请使用完整 ID');
218
+ if (matches.length === 1)
219
+ fromFilter = matches[0];
220
+ }
221
+ }
222
+ if (string('unread') === 'true')
223
+ throw new CollaborationError('READ_RECEIPTS_UNSUPPORTED', 'Use a consumer cursor to track retrieved messages');
224
+ const page = store.page(sessionId, { since, afterId: string('after_id'), cursor: string('cursor'),
189
225
  consumer: string('consumer'), limit: string('limit') ? Number(string('limit')) : undefined,
190
- from: string('from'), group: groupFilter, thread: threadFilter, kind: string('kind'), responseKind: string('response_kind'), order: string('order') });
226
+ from: fromFilter, group: groupFilter, thread: threadFilter, kind: string('kind'), responseKind: string('response_kind'), order: string('order') });
227
+ const messages = page.messages.map(message => terminalMessage(message, string('raw') === 'true'));
191
228
  // Fan-out edges carry sibling recipients; the names map lets the CLI text
192
229
  // renderer show 同时发给了:… as people instead of bare session ids. The
193
230
  // fallback to the raw id stays server-side agnostic (federated/offline).
194
231
  const nameIds = [...new Set(page.messages.flatMap((message) => [message.fromSessionId, ...(message.fanOutIds ?? [])].filter((id) => typeof id === 'string' && id.length > 0)))];
195
- res.json({ ok: true, ...page, ...(nameIds.length ? { names: resolveNames?.(nameIds) ?? {} } : {}) });
232
+ res.json({ ok: true, ...page, messages, ...(nameIds.length ? { names: resolveNames?.(nameIds) ?? {} } : {}) });
196
233
  }));
197
234
  router.post('/cursor/commit', run((req, res, sessionId) => {
198
235
  store.commitCursor(sessionId, String(req.body.cursor ?? ''), String(req.body.consumer ?? ''));
@@ -212,6 +249,16 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
212
249
  // Member names ride along so role lists read as people, not bare ids; the
213
250
  // caller falls back to the session id when a name is unknown (federated or
214
251
  // offline members have no local record).
252
+ const memberOf = (ids, input) => {
253
+ if (ids.includes(input))
254
+ return input;
255
+ if (input.length < 4)
256
+ return null;
257
+ const matches = ids.filter(id => id.startsWith(input) || (id.startsWith('remote:') && decodeURIComponent(id.slice(id.lastIndexOf(':') + 1)).startsWith(input)));
258
+ if (matches.length > 1)
259
+ throw new CollaborationError('SESSION_ID_AMBIGUOUS', '成员短 ID 匹配多个会话,请使用完整 ID');
260
+ return matches[0] ?? null;
261
+ };
215
262
  const groupView = (group) => {
216
263
  const names = resolveNames?.(group.sessionIds) ?? {};
217
264
  return {
@@ -220,6 +267,18 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
220
267
  roles: group.roles ?? {},
221
268
  };
222
269
  };
270
+ router.get('/rules', run((req, res, sessionId) => {
271
+ const group = groupOf(String(req.query.group ?? ''), sessionId);
272
+ res.json({ ok: true, group_id: group.id, instructions: group.instructions ?? null });
273
+ }));
274
+ router.post('/rules', run((req, res, sessionId) => {
275
+ const group = groupOf(String(req.body.group_id ?? ''), sessionId);
276
+ const expected = req.body.expected_version;
277
+ if (expected !== undefined && typeof expected !== 'string')
278
+ throw new Error('INVALID_VERSION');
279
+ const instructions = store.setRules(group.id, req.body.text, sessionId, expected);
280
+ res.json({ ok: true, group_id: group.id, instructions });
281
+ }));
223
282
  router.get('/role', run((req, res, sessionId) => {
224
283
  const groupId = String(req.query.group ?? '');
225
284
  if (groupId) {
@@ -236,13 +295,14 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
236
295
  router.post('/role', run((req, res, sessionId) => {
237
296
  const group = groupOf(String(req.body.group_id ?? ''), sessionId);
238
297
  const role = req.body.role;
239
- if (typeof req.body.session_id !== 'string' || !group.sessionIds.includes(req.body.session_id)) {
298
+ const member = typeof req.body.session_id === 'string' ? memberOf(group.sessionIds, req.body.session_id) : null;
299
+ if (!member) {
240
300
  throw new CollaborationError('NOT_A_MEMBER', 'Role target must be a member of this group', 400);
241
301
  }
242
302
  if (role === undefined || (role !== null && typeof role !== 'string')) {
243
303
  throw new CollaborationError('INVALID_ROLE', 'role must be text or null', 400);
244
304
  }
245
- const updated = store.setRole({ groupId: group.id, sessionId: req.body.session_id, role: role });
305
+ const updated = store.setRole({ groupId: group.id, sessionId: member, role: role });
246
306
  res.json({ ok: true, group: { ...updated, roles: updated.roles ?? {} } });
247
307
  }));
248
308
  router.post('/name', run(async (req, res, sessionId) => {