@shaykec/bridge 0.4.20 → 0.4.22

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 (317) hide show
  1. package/README.md +2 -2
  2. package/canvas-dist/assets/{_basePickBy-CWoeT3J7.js → _basePickBy-BovdgFIW.js} +1 -1
  3. package/canvas-dist/assets/_basePickBy-BtkHe2u_.js +1 -0
  4. package/canvas-dist/assets/_basePickBy-C0936578.js +1 -0
  5. package/canvas-dist/assets/_basePickBy-CE2Qvuh7.js +1 -0
  6. package/canvas-dist/assets/_basePickBy-DV6sX4CG.js +1 -0
  7. package/canvas-dist/assets/_basePickBy-DZX6ZNMT.js +1 -0
  8. package/canvas-dist/assets/{_baseUniq-Dtuvtwtn.js → _baseUniq-B7dN28TM.js} +1 -1
  9. package/canvas-dist/assets/_baseUniq-Cl23fCdR.js +1 -0
  10. package/canvas-dist/assets/_baseUniq-CojWFw7B.js +1 -0
  11. package/canvas-dist/assets/_baseUniq-DA640BJl.js +1 -0
  12. package/canvas-dist/assets/_baseUniq-Ds-62CCj.js +1 -0
  13. package/canvas-dist/assets/_baseUniq-KG7SRw9H.js +1 -0
  14. package/canvas-dist/assets/{arc-YYWnrNJU.js → arc-7E9FFKlC.js} +1 -1
  15. package/canvas-dist/assets/arc-BSMfRZtt.js +1 -0
  16. package/canvas-dist/assets/arc-C6nT-koR.js +1 -0
  17. package/canvas-dist/assets/arc-D_fOnjmo.js +1 -0
  18. package/canvas-dist/assets/arc-Khfvgkr3.js +1 -0
  19. package/canvas-dist/assets/arc-ieS-i42x.js +1 -0
  20. package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-CegbV-RR.js → architectureDiagram-VXUJARFQ-DF4t6GQD.js} +1 -1
  21. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DXgSlsio.js +36 -0
  22. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DiomxPB4.js +36 -0
  23. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DnFaxvXD.js +36 -0
  24. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-Dt38C0LJ.js +36 -0
  25. package/canvas-dist/assets/architectureDiagram-VXUJARFQ-egbtMwua.js +36 -0
  26. package/canvas-dist/assets/{blockDiagram-VD42YOAC-C2e_j6ry.js → blockDiagram-VD42YOAC-CUNKQd-b.js} +1 -1
  27. package/canvas-dist/assets/blockDiagram-VD42YOAC-D-NiLXxd.js +122 -0
  28. package/canvas-dist/assets/blockDiagram-VD42YOAC-Dx6Dh9gg.js +122 -0
  29. package/canvas-dist/assets/blockDiagram-VD42YOAC-_r-PmlQy.js +122 -0
  30. package/canvas-dist/assets/blockDiagram-VD42YOAC-bvYKZLMc.js +122 -0
  31. package/canvas-dist/assets/blockDiagram-VD42YOAC-l85QT9Ig.js +122 -0
  32. package/canvas-dist/assets/{c4Diagram-YG6GDRKO-rIpnAud9.js → c4Diagram-YG6GDRKO-BWKCTyQi.js} +1 -1
  33. package/canvas-dist/assets/c4Diagram-YG6GDRKO-CbXs2xzC.js +10 -0
  34. package/canvas-dist/assets/c4Diagram-YG6GDRKO-CjiS-GNK.js +10 -0
  35. package/canvas-dist/assets/c4Diagram-YG6GDRKO-D7SnLlHp.js +10 -0
  36. package/canvas-dist/assets/c4Diagram-YG6GDRKO-RTTCSVf2.js +10 -0
  37. package/canvas-dist/assets/c4Diagram-YG6GDRKO-yvqJ_AqX.js +10 -0
  38. package/canvas-dist/assets/channel-CSXq7GP6.js +1 -0
  39. package/canvas-dist/assets/channel-CvujjGiJ.js +1 -0
  40. package/canvas-dist/assets/channel-D959Iony.js +1 -0
  41. package/canvas-dist/assets/channel-DOSwCnrB.js +1 -0
  42. package/canvas-dist/assets/channel-sw61LzxF.js +1 -0
  43. package/canvas-dist/assets/channel-vZVnNhOK.js +1 -0
  44. package/canvas-dist/assets/{chunk-4BX2VUAB-CpZGetnU.js → chunk-4BX2VUAB-BBjuAwXr.js} +1 -1
  45. package/canvas-dist/assets/chunk-4BX2VUAB-BXRNyucU.js +1 -0
  46. package/canvas-dist/assets/chunk-4BX2VUAB-Bgq5Z77T.js +1 -0
  47. package/canvas-dist/assets/chunk-4BX2VUAB-BuoMCMCr.js +1 -0
  48. package/canvas-dist/assets/chunk-4BX2VUAB-COD5n7vg.js +1 -0
  49. package/canvas-dist/assets/chunk-4BX2VUAB-K8DepKJO.js +1 -0
  50. package/canvas-dist/assets/{chunk-55IACEB6-L0OhcFdd.js → chunk-55IACEB6-Bic_bMrQ.js} +1 -1
  51. package/canvas-dist/assets/chunk-55IACEB6-DEy2QUDq.js +1 -0
  52. package/canvas-dist/assets/chunk-55IACEB6-Dcgbmfzg.js +1 -0
  53. package/canvas-dist/assets/chunk-55IACEB6-DfmuNm_E.js +1 -0
  54. package/canvas-dist/assets/chunk-55IACEB6-DlQRcczm.js +1 -0
  55. package/canvas-dist/assets/chunk-55IACEB6-p2qMY-fm.js +1 -0
  56. package/canvas-dist/assets/{chunk-B4BG7PRW-Cv9vsAzg.js → chunk-B4BG7PRW-BpbyxBP2.js} +1 -1
  57. package/canvas-dist/assets/chunk-B4BG7PRW-CCPqvPrP.js +165 -0
  58. package/canvas-dist/assets/chunk-B4BG7PRW-CEeDPAki.js +165 -0
  59. package/canvas-dist/assets/chunk-B4BG7PRW-D2UFN_2M.js +165 -0
  60. package/canvas-dist/assets/chunk-B4BG7PRW-DFI5h6HC.js +165 -0
  61. package/canvas-dist/assets/chunk-B4BG7PRW-DKOiFGMU.js +165 -0
  62. package/canvas-dist/assets/{chunk-DI55MBZ5-B3p1mU43.js → chunk-DI55MBZ5-BV6nHjNQ.js} +1 -1
  63. package/canvas-dist/assets/chunk-DI55MBZ5-CEZJmC0E.js +220 -0
  64. package/canvas-dist/assets/chunk-DI55MBZ5-DOZT99Ek.js +220 -0
  65. package/canvas-dist/assets/chunk-DI55MBZ5-DmC2LoG2.js +220 -0
  66. package/canvas-dist/assets/chunk-DI55MBZ5-DpkcJdZP.js +220 -0
  67. package/canvas-dist/assets/chunk-DI55MBZ5-fVTGx0zh.js +220 -0
  68. package/canvas-dist/assets/{chunk-FMBD7UC4-JCLAHw5x.js → chunk-FMBD7UC4-BOCyQpI7.js} +1 -1
  69. package/canvas-dist/assets/chunk-FMBD7UC4-C76FrRL8.js +15 -0
  70. package/canvas-dist/assets/chunk-FMBD7UC4-CAq-btWc.js +15 -0
  71. package/canvas-dist/assets/chunk-FMBD7UC4-CidVsej6.js +15 -0
  72. package/canvas-dist/assets/chunk-FMBD7UC4-DPpfskdX.js +15 -0
  73. package/canvas-dist/assets/chunk-FMBD7UC4-DnLtclge.js +15 -0
  74. package/canvas-dist/assets/{chunk-QN33PNHL-C9arKEVq.js → chunk-QN33PNHL-BclpCUi8.js} +1 -1
  75. package/canvas-dist/assets/chunk-QN33PNHL-DDUw8IU1.js +1 -0
  76. package/canvas-dist/assets/chunk-QN33PNHL-DdJFAUXw.js +1 -0
  77. package/canvas-dist/assets/chunk-QN33PNHL-DjV4jUn9.js +1 -0
  78. package/canvas-dist/assets/chunk-QN33PNHL-N-HTycqU.js +1 -0
  79. package/canvas-dist/assets/chunk-QN33PNHL-sd8p21DW.js +1 -0
  80. package/canvas-dist/assets/{chunk-QZHKN3VN-Bs1r3d9U.js → chunk-QZHKN3VN-B6mT-JkP.js} +1 -1
  81. package/canvas-dist/assets/chunk-QZHKN3VN-BCo8pc7x.js +1 -0
  82. package/canvas-dist/assets/chunk-QZHKN3VN-C8IIu6es.js +1 -0
  83. package/canvas-dist/assets/chunk-QZHKN3VN-D9FF492U.js +1 -0
  84. package/canvas-dist/assets/chunk-QZHKN3VN-DWMbUjXT.js +1 -0
  85. package/canvas-dist/assets/chunk-QZHKN3VN-l5FBJ77g.js +1 -0
  86. package/canvas-dist/assets/{chunk-TZMSLE5B-_Ye6r84Y.js → chunk-TZMSLE5B-BASt-UWt.js} +1 -1
  87. package/canvas-dist/assets/chunk-TZMSLE5B-BCfaZWLT.js +1 -0
  88. package/canvas-dist/assets/chunk-TZMSLE5B-BKIk_hBR.js +1 -0
  89. package/canvas-dist/assets/chunk-TZMSLE5B-C4pt-Ir8.js +1 -0
  90. package/canvas-dist/assets/chunk-TZMSLE5B-DwGlELvo.js +1 -0
  91. package/canvas-dist/assets/chunk-TZMSLE5B-jJKG-WvJ.js +1 -0
  92. package/canvas-dist/assets/classDiagram-2ON5EDUG-B7YQfPU4.js +1 -0
  93. package/canvas-dist/assets/classDiagram-2ON5EDUG-BZ61MaHY.js +1 -0
  94. package/canvas-dist/assets/classDiagram-2ON5EDUG-CGseYor2.js +1 -0
  95. package/canvas-dist/assets/classDiagram-2ON5EDUG-CKzOc99J.js +1 -0
  96. package/canvas-dist/assets/classDiagram-2ON5EDUG-Ce_LPjwW.js +1 -0
  97. package/canvas-dist/assets/classDiagram-2ON5EDUG-DorPdibv.js +1 -0
  98. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-B7YQfPU4.js +1 -0
  99. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BZ61MaHY.js +1 -0
  100. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CGseYor2.js +1 -0
  101. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CKzOc99J.js +1 -0
  102. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-Ce_LPjwW.js +1 -0
  103. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-DorPdibv.js +1 -0
  104. package/canvas-dist/assets/clone-74KSto7H.js +1 -0
  105. package/canvas-dist/assets/clone-CJQgAYVe.js +1 -0
  106. package/canvas-dist/assets/clone-DLeTuhHE.js +1 -0
  107. package/canvas-dist/assets/clone-D_IHK_lQ.js +1 -0
  108. package/canvas-dist/assets/clone-DxMUv1L9.js +1 -0
  109. package/canvas-dist/assets/clone-UNKf_nED.js +1 -0
  110. package/canvas-dist/assets/{cose-bilkent-S5V4N54A-2O2oovOj.js → cose-bilkent-S5V4N54A-BTyQiCkr.js} +1 -1
  111. package/canvas-dist/assets/cose-bilkent-S5V4N54A-BtPAe24N.js +1 -0
  112. package/canvas-dist/assets/cose-bilkent-S5V4N54A-DIjE7V3m.js +1 -0
  113. package/canvas-dist/assets/cose-bilkent-S5V4N54A-DKL_BGvE.js +1 -0
  114. package/canvas-dist/assets/cose-bilkent-S5V4N54A-LZ4OsCLU.js +1 -0
  115. package/canvas-dist/assets/cose-bilkent-S5V4N54A-XWeJtgga.js +1 -0
  116. package/canvas-dist/assets/{dagre-6UL2VRFP-gRmGLrEW.js → dagre-6UL2VRFP-BJ2vcFwR.js} +1 -1
  117. package/canvas-dist/assets/dagre-6UL2VRFP-C1FlE5s8.js +4 -0
  118. package/canvas-dist/assets/dagre-6UL2VRFP-C3BWFgl6.js +4 -0
  119. package/canvas-dist/assets/dagre-6UL2VRFP-CUnx73Rf.js +4 -0
  120. package/canvas-dist/assets/dagre-6UL2VRFP-Do10BY1y.js +4 -0
  121. package/canvas-dist/assets/dagre-6UL2VRFP-rOZEkrsg.js +4 -0
  122. package/canvas-dist/assets/{diagram-PSM6KHXK-B7Li-xxw.js → diagram-PSM6KHXK-BGi_qzbq.js} +1 -1
  123. package/canvas-dist/assets/diagram-PSM6KHXK-C3Nv7h_j.js +24 -0
  124. package/canvas-dist/assets/diagram-PSM6KHXK-CsMy-r0n.js +24 -0
  125. package/canvas-dist/assets/diagram-PSM6KHXK-Dj8g7kGt.js +24 -0
  126. package/canvas-dist/assets/diagram-PSM6KHXK-Dxb1w_7r.js +24 -0
  127. package/canvas-dist/assets/diagram-PSM6KHXK-kVMBkEyV.js +24 -0
  128. package/canvas-dist/assets/{diagram-QEK2KX5R-B_NNUAm3.js → diagram-QEK2KX5R-4bsrr1WZ.js} +1 -1
  129. package/canvas-dist/assets/diagram-QEK2KX5R-Bv7BmKfI.js +43 -0
  130. package/canvas-dist/assets/diagram-QEK2KX5R-C_FLN6hv.js +43 -0
  131. package/canvas-dist/assets/diagram-QEK2KX5R-Csuk5L3z.js +43 -0
  132. package/canvas-dist/assets/diagram-QEK2KX5R-D5Aszgz4.js +43 -0
  133. package/canvas-dist/assets/diagram-QEK2KX5R-DX58f87l.js +43 -0
  134. package/canvas-dist/assets/{diagram-S2PKOQOG-NcK-KHaA.js → diagram-S2PKOQOG-1Q7hwiSd.js} +1 -1
  135. package/canvas-dist/assets/diagram-S2PKOQOG-Bz9Vxi5V.js +24 -0
  136. package/canvas-dist/assets/diagram-S2PKOQOG-CdWgZIIc.js +24 -0
  137. package/canvas-dist/assets/diagram-S2PKOQOG-DBicbKFU.js +24 -0
  138. package/canvas-dist/assets/diagram-S2PKOQOG-DsXKwPtU.js +24 -0
  139. package/canvas-dist/assets/diagram-S2PKOQOG-L_SMHLXs.js +24 -0
  140. package/canvas-dist/assets/{erDiagram-Q2GNP2WA-CG7dqzk3.js → erDiagram-Q2GNP2WA-BYu7fh6H.js} +1 -1
  141. package/canvas-dist/assets/erDiagram-Q2GNP2WA-CvnQ69BF.js +60 -0
  142. package/canvas-dist/assets/erDiagram-Q2GNP2WA-D3xm-Tdm.js +60 -0
  143. package/canvas-dist/assets/erDiagram-Q2GNP2WA-DIPpD8sj.js +60 -0
  144. package/canvas-dist/assets/erDiagram-Q2GNP2WA-DNgu6dMd.js +60 -0
  145. package/canvas-dist/assets/erDiagram-Q2GNP2WA-Decm8aB4.js +60 -0
  146. package/canvas-dist/assets/{flowDiagram-NV44I4VS-CBzCj5D6.js → flowDiagram-NV44I4VS-2ymk2kw2.js} +1 -1
  147. package/canvas-dist/assets/flowDiagram-NV44I4VS-BEPFOt6U.js +162 -0
  148. package/canvas-dist/assets/flowDiagram-NV44I4VS-BwqXYGfK.js +162 -0
  149. package/canvas-dist/assets/flowDiagram-NV44I4VS-CS1jax_z.js +162 -0
  150. package/canvas-dist/assets/flowDiagram-NV44I4VS-DQz5bf7r.js +162 -0
  151. package/canvas-dist/assets/flowDiagram-NV44I4VS-KW4T1sqF.js +162 -0
  152. package/canvas-dist/assets/{ganttDiagram-JELNMOA3-CHw-4qJC.js → ganttDiagram-JELNMOA3-B811prZt.js} +1 -1
  153. package/canvas-dist/assets/ganttDiagram-JELNMOA3-C75pWm7X.js +267 -0
  154. package/canvas-dist/assets/ganttDiagram-JELNMOA3-CWsbo0fn.js +267 -0
  155. package/canvas-dist/assets/ganttDiagram-JELNMOA3-CbJozPBN.js +267 -0
  156. package/canvas-dist/assets/ganttDiagram-JELNMOA3-Co0cFt4c.js +267 -0
  157. package/canvas-dist/assets/ganttDiagram-JELNMOA3-I4PDqrRh.js +267 -0
  158. package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-Dqrc4wUs.js → gitGraphDiagram-V2S2FVAM-B-z0cLPt.js} +1 -1
  159. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-Be40z-LF.js +65 -0
  160. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BejNaAVm.js +65 -0
  161. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BqWDYr0X.js +65 -0
  162. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-DSvWGY-e.js +65 -0
  163. package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-HLYbyNJ5.js +65 -0
  164. package/canvas-dist/assets/{graph-X9Kzu-pf.js → graph-BE8KKsdf.js} +1 -1
  165. package/canvas-dist/assets/graph-D6DzzszU.js +1 -0
  166. package/canvas-dist/assets/graph-DFR8Y_8s.js +1 -0
  167. package/canvas-dist/assets/graph-Da385cDY.js +1 -0
  168. package/canvas-dist/assets/graph-MU7gZz2B.js +1 -0
  169. package/canvas-dist/assets/graph-wjSBJwnf.js +1 -0
  170. package/canvas-dist/assets/index--ztw-8Rw.js +647 -0
  171. package/canvas-dist/assets/{index-BQFKo-II.js → index-5TpIM6B1.js} +1 -1
  172. package/canvas-dist/assets/index-6GBZ9nXN.css +32 -0
  173. package/canvas-dist/assets/index-BSswTuBk.js +11 -0
  174. package/canvas-dist/assets/index-BVvhMmjs.js +11 -0
  175. package/canvas-dist/assets/index-BY92Mj5g.js +572 -0
  176. package/canvas-dist/assets/index-CV7palC3.js +572 -0
  177. package/canvas-dist/assets/index-D9bmQGsB.js +11 -0
  178. package/canvas-dist/assets/index-DDIKkGv8.js +592 -0
  179. package/canvas-dist/assets/index-Dyo0NkPb.js +574 -0
  180. package/canvas-dist/assets/index-iQWajCow.js +572 -0
  181. package/canvas-dist/assets/index-m68YlAMU.js +11 -0
  182. package/canvas-dist/assets/index-mEoP57az.js +11 -0
  183. package/canvas-dist/assets/{infoDiagram-HS3SLOUP-CflnZPsm.js → infoDiagram-HS3SLOUP--9BirqgJ.js} +1 -1
  184. package/canvas-dist/assets/infoDiagram-HS3SLOUP-CSJVED2y.js +2 -0
  185. package/canvas-dist/assets/infoDiagram-HS3SLOUP-D68HIb2t.js +2 -0
  186. package/canvas-dist/assets/infoDiagram-HS3SLOUP-DK2VLGGz.js +2 -0
  187. package/canvas-dist/assets/infoDiagram-HS3SLOUP-PaFhn4yD.js +2 -0
  188. package/canvas-dist/assets/infoDiagram-HS3SLOUP-zLNG47sU.js +2 -0
  189. package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D2gkCipQ.js → journeyDiagram-XKPGCS4Q-Bue2dR2X.js} +1 -1
  190. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-CrgZfpdU.js +139 -0
  191. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-DUxWmkkC.js +139 -0
  192. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-OTFkv4pd.js +139 -0
  193. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-eK2_Zuu3.js +139 -0
  194. package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-uds5Tz8D.js +139 -0
  195. package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CtLLz4o8.js → kanban-definition-3W4ZIXB7-BETdiI7I.js} +1 -1
  196. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-BdVh7KdN.js +89 -0
  197. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-Cxl8UM9S.js +89 -0
  198. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-DVPlx3I2.js +89 -0
  199. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-LtNWeoYB.js +89 -0
  200. package/canvas-dist/assets/kanban-definition-3W4ZIXB7-uvhEMvyE.js +89 -0
  201. package/canvas-dist/assets/{layout-CjvV_Dms.js → layout-1OzszN14.js} +1 -1
  202. package/canvas-dist/assets/layout-CJSupFcF.js +1 -0
  203. package/canvas-dist/assets/layout-DFRmxN_c.js +1 -0
  204. package/canvas-dist/assets/layout-DSu-zk7y.js +1 -0
  205. package/canvas-dist/assets/layout-TGcrvApd.js +1 -0
  206. package/canvas-dist/assets/layout-eStc8SYK.js +1 -0
  207. package/canvas-dist/assets/{linear-D3cIYHoS.js → linear-9qlE6xa7.js} +1 -1
  208. package/canvas-dist/assets/linear-CBfFWnLD.js +1 -0
  209. package/canvas-dist/assets/linear-Cv4ai8Hq.js +1 -0
  210. package/canvas-dist/assets/linear-DDzz65E6.js +1 -0
  211. package/canvas-dist/assets/linear-wbIqhwDf.js +1 -0
  212. package/canvas-dist/assets/linear-wyNKl76F.js +1 -0
  213. package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-DSgjVg-P.js → mindmap-definition-VGOIOE7T-3l4YzhEM.js} +1 -1
  214. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-B-KkpNlw.js +68 -0
  215. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-DHMHWgmT.js +68 -0
  216. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-Dqfyg4Z2.js +68 -0
  217. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-NeRYOzsq.js +68 -0
  218. package/canvas-dist/assets/mindmap-definition-VGOIOE7T-xyu628P9.js +68 -0
  219. package/canvas-dist/assets/{pieDiagram-ADFJNKIX-B_lYaGFj.js → pieDiagram-ADFJNKIX-BWNzVAGj.js} +1 -1
  220. package/canvas-dist/assets/pieDiagram-ADFJNKIX-Bm3PXYs-.js +30 -0
  221. package/canvas-dist/assets/pieDiagram-ADFJNKIX-BvvN7VvQ.js +30 -0
  222. package/canvas-dist/assets/pieDiagram-ADFJNKIX-BwU7AN7W.js +30 -0
  223. package/canvas-dist/assets/pieDiagram-ADFJNKIX-CHgwWCaM.js +30 -0
  224. package/canvas-dist/assets/pieDiagram-ADFJNKIX-DlZc8YOh.js +30 -0
  225. package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-DLZLTJe3.js → quadrantDiagram-AYHSOK5B-B-Zd8OFp.js} +1 -1
  226. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-B1CnJyxI.js +7 -0
  227. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C0Qo00b9.js +7 -0
  228. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C9bx3nEJ.js +7 -0
  229. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-UHENkiRO.js +7 -0
  230. package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-jKfurTPU.js +7 -0
  231. package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CZE26rhL.js → requirementDiagram-UZGBJVZJ-BPpNNusD.js} +1 -1
  232. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-BwZF1NIK.js +64 -0
  233. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-CaT3Frtk.js +64 -0
  234. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-Dfoz7R_7.js +64 -0
  235. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-DsrX4TT-.js +64 -0
  236. package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-dmouSXOl.js +64 -0
  237. package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-DQMRJAPV.js → sankeyDiagram-TZEHDZUN-BEy-A1Fu.js} +1 -1
  238. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BViMBiAQ.js +10 -0
  239. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BqrM-qWN.js +10 -0
  240. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DRkRC9qB.js +10 -0
  241. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DbuzKCtn.js +10 -0
  242. package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-_aHMKbpw.js +10 -0
  243. package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-BY723FEn.js → sequenceDiagram-WL72ISMW-B8FOaL2Q.js} +1 -1
  244. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-C02NQwOB.js +145 -0
  245. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CgyHivPj.js +145 -0
  246. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CzW1WaEm.js +145 -0
  247. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-DJhHI1pe.js +145 -0
  248. package/canvas-dist/assets/sequenceDiagram-WL72ISMW-VFkpAeoG.js +145 -0
  249. package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-C_UdOFhy.js → stateDiagram-FKZM4ZOC-BSqFX4PJ.js} +1 -1
  250. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-BnXhhxkN.js +1 -0
  251. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-ClARVrvt.js +1 -0
  252. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-CuC6xesY.js +1 -0
  253. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-DcAiGjph.js +1 -0
  254. package/canvas-dist/assets/stateDiagram-FKZM4ZOC-aBg0hjTp.js +1 -0
  255. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-8fib9ftc.js +1 -0
  256. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-B-DO0ZqO.js +1 -0
  257. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-BksbsE4k.js +1 -0
  258. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-C2DJCNPK.js +1 -0
  259. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-CeA5jba6.js +1 -0
  260. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-zsAyq0tK.js +1 -0
  261. package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DkrJqww0.js → timeline-definition-IT6M3QCI-BaHdYD2h.js} +1 -1
  262. package/canvas-dist/assets/timeline-definition-IT6M3QCI-Bl2hg8IM.js +61 -0
  263. package/canvas-dist/assets/timeline-definition-IT6M3QCI-CrVwLiGm.js +61 -0
  264. package/canvas-dist/assets/timeline-definition-IT6M3QCI-DrXGRjnB.js +61 -0
  265. package/canvas-dist/assets/timeline-definition-IT6M3QCI-cYAwshf6.js +61 -0
  266. package/canvas-dist/assets/timeline-definition-IT6M3QCI-flyL0y-3.js +61 -0
  267. package/canvas-dist/assets/{treemap-GDKQZRPO-B-6bMZqD.js → treemap-GDKQZRPO-C4Hg8kJ_.js} +1 -1
  268. package/canvas-dist/assets/treemap-GDKQZRPO-DVY2G9qY.js +162 -0
  269. package/canvas-dist/assets/treemap-GDKQZRPO-DpLWPA1z.js +162 -0
  270. package/canvas-dist/assets/treemap-GDKQZRPO-Ds86cUVw.js +162 -0
  271. package/canvas-dist/assets/treemap-GDKQZRPO-DwmoI6tH.js +162 -0
  272. package/canvas-dist/assets/treemap-GDKQZRPO-SsGFkgVd.js +162 -0
  273. package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DkBhUy_D.js → xychartDiagram-PRI3JC2R-B9c1iLBf.js} +1 -1
  274. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-BpX6MPWa.js +7 -0
  275. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CEgW_j0p.js +7 -0
  276. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CSEFGEQX.js +7 -0
  277. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CnG4XoMc.js +7 -0
  278. package/canvas-dist/assets/xychartDiagram-PRI3JC2R-Dftj3Bt3.js +7 -0
  279. package/canvas-dist/index.html +3 -2
  280. package/package.json +3 -2
  281. package/src/protocol.js +1 -1
  282. package/src/protocol.test.js +1 -1
  283. package/src/pty-manager.js +281 -0
  284. package/src/pty-manager.test.js +212 -0
  285. package/src/router.js +31 -1
  286. package/src/router.test.js +1 -1
  287. package/src/sdk-e2e.test.js +101 -0
  288. package/src/server.e2e.test.js +804 -138
  289. package/src/server.js +1273 -137
  290. package/src/session-store.js +260 -0
  291. package/src/session-store.test.js +235 -0
  292. package/src/templates.js +1 -1
  293. package/src/templates.test.js +1 -1
  294. package/src/terminal.js +3 -3
  295. package/src/terminal.test.js +12 -12
  296. package/src/visual-interceptor.js +450 -0
  297. package/src/visual-interceptor.test.js +943 -0
  298. package/src/workshop-parser.js +251 -0
  299. package/src/workshop-parser.test.js +179 -0
  300. package/templates/celebrate.html +1 -1
  301. package/templates/code-playground.html +1 -1
  302. package/templates/dashboard.html +7 -8
  303. package/templates/diagram-architecture.html +1 -1
  304. package/templates/diagram-flow.html +1 -1
  305. package/templates/diagram-mermaid.html +1 -1
  306. package/templates/game-speed-round.html +1 -1
  307. package/templates/quiz-drag-order.html +1 -1
  308. package/templates/quiz-fill-blank.html +1 -1
  309. package/templates/quiz-matching.html +1 -1
  310. package/templates/quiz-timed-choice.html +1 -1
  311. package/templates/welcome.html +7 -7
  312. package/canvas-dist/assets/channel-BzJVlie3.js +0 -1
  313. package/canvas-dist/assets/classDiagram-2ON5EDUG-BTs-zEmB.js +0 -1
  314. package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BTs-zEmB.js +0 -1
  315. package/canvas-dist/assets/clone-CXEfuXmc.js +0 -1
  316. package/canvas-dist/assets/index-DJ49c6u-.js +0 -426
  317. package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-DXIiFh0L.js +0 -1
package/src/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * ClaudeTeach Bridge Server — WebSocket + SSE + REST communication hub.
2
+ * Socrates Bridge Server — WebSocket + SSE + REST communication hub.
3
3
  * Upgraded from packages/core/src/bridge-server.js.
4
4
  *
5
5
  * Endpoints:
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import { createServer } from 'http';
24
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
24
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, createReadStream } from 'fs';
25
25
  import { join, dirname, extname, resolve } from 'path';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { exec } from 'child_process';
@@ -34,17 +34,37 @@ import {
34
34
  serializeEnvelope,
35
35
  MSG_SYS_CONNECT,
36
36
  MSG_SYS_DISCONNECT,
37
+ MSG_EVENT_PTY_OUTPUT,
37
38
  DEFAULT_PORT,
38
39
  INBOX_DIR,
39
40
  MAX_LONG_POLL_TIMEOUT_MS,
40
41
  ACHIEVEMENTS,
42
+ BELTS,
43
+ PLAYGROUND_EXEC_TIMEOUT_MS,
44
+ PLAYGROUND_MAX_OUTPUT_BYTES,
41
45
  } from '@shaykec/shared';
42
46
 
43
47
  import { TierManager, generateClientId, validateHandshake } from './protocol.js';
44
48
  import { EventRouter } from './router.js';
45
49
  import { renderTemplate, listTemplates } from './templates.js';
46
50
  import { createSession, execCommand, getSession, destroySession, cleanupIdleSessions } from './terminal.js';
51
+ import {
52
+ createPtySession, writeToPty, resizePty, setPtyCallbacks,
53
+ hasPtySession, destroyPtySession, writeSessionFiles, runSetupCommands,
54
+ cleanupIdlePtySessions, getPtyOutputBuffer,
55
+ } from './pty-manager.js';
56
+ import { extractAndRouteVisuals, extractAndRouteMedia, extractAndRouteSuggestions, extractButtons, extractInlineBlocks, enhanceWithSmartComponents, stripCanvasCommands } from './visual-interceptor.js';
47
57
  import { createAgentServer } from '@shaykec/agent-web/server';
58
+ import {
59
+ listSessions as listStoredSessions,
60
+ getSession as getStoredSession,
61
+ appendMessage as appendStoredMessage,
62
+ updateMeta as updateStoredMeta,
63
+ deleteSession as deleteStoredSession,
64
+ purgeAll as purgeStoredSessions,
65
+ detectSessionTag,
66
+ generateTitle,
67
+ } from './session-store.js';
48
68
 
49
69
  const __filename = fileURLToPath(import.meta.url);
50
70
  const __dirname = dirname(__filename);
@@ -73,12 +93,12 @@ const MIME_TYPES = {
73
93
  };
74
94
 
75
95
  /**
76
- * Start the ClaudeTeach bridge server.
96
+ * Start the Socrates bridge server.
77
97
  * @param {object} [options]
78
98
  * @param {number} [options.port=3456] - Port number
79
99
  * @param {function} [options.onReady] - Callback when server is listening
80
100
  * @param {function} [options.onTierChange] - Callback(oldTier, newTier) on tier changes
81
- * @param {object} [options.progressProvider] - Object with getProgress() method
101
+ * @param {object} [options.progressProvider] - Object with getProgress() and saveProgress() methods
82
102
  * @returns {{ server: object, tierManager: TierManager, router: EventRouter, close: function }}
83
103
  */
84
104
  export function startServer(options = {}) {
@@ -87,24 +107,217 @@ export function startServer(options = {}) {
87
107
  const router = new EventRouter(tierManager);
88
108
 
89
109
  // Chat is delegated entirely to @shaykec/agent-web
90
- const agentConfig = { cwd: process.cwd() };
110
+ const agentConfig = {
111
+ cwd: process.cwd(),
112
+ systemPrompt: [
113
+ 'You are Socrates, a Socratic AI tutor. You teach through guided dialogue.',
114
+ '',
115
+ 'RULES:',
116
+ '- Ask, don\'t tell. Guide the user to discover answers through questions.',
117
+ '- The user writes all code. You guide, they do.',
118
+ '- One step at a time. Award XP after milestones.',
119
+ '',
120
+ 'ENVIRONMENT:',
121
+ '- The user is in a web app with a visual canvas beside the chat.',
122
+ '- A bridge server at http://localhost:3456 has visual tools.',
123
+ '- When a user sends /teach <topic>, use the Skill tool to read the "teach" skill, then follow its instructions.',
124
+ '- When a user sends /learn <topic>, use the Skill tool to read the "learn" skill, then follow its instructions.',
125
+ '- For /stats, /ask, /canvas, /level-up — use the Skill tool to read the matching skill name.',
126
+ '- For casual questions, answer helpfully. Include mermaid diagrams when useful —',
127
+ ' they auto-appear on the visual canvas.',
128
+ '',
129
+ 'MANDATORY — RICH COMPONENTS IN EVERY TEACHING RESPONSE:',
130
+ 'You MUST use the smart components below in EVERY teaching response. Plain markdown alone is NOT acceptable.',
131
+ 'Each teaching response MUST include at least 3 of these components. The canvas renders them as interactive UI.',
132
+ '',
133
+ 'SMART BUTTONS (REQUIRED for questions and choices):',
134
+ '- ALWAYS use buttons when asking the user a question or presenting choices.',
135
+ '- Embed inline buttons using an HTML comment:',
136
+ ' <!-- buttons: {"id":"unique-id","type":"single","prompt":"Pick one:","options":[{"label":"Option A","value":"text sent when clicked"}]} -->',
137
+ '- Types: "single" (pick one), "multi" (pick several + submit), "rating" (1-5 scale).',
138
+ '- Each option needs "label" (display text) and "value" (message sent on click).',
139
+ '- Use buttons for contextual choices (topic selection, difficulty rating, yes/no).',
140
+ '- Use suggestion chips (<!-- suggestions: [...] -->) for general navigation.',
141
+ '',
142
+ 'SMART LISTS (REQUIRED for any list of items):',
143
+ '- NEVER use plain markdown lists. ALWAYS use smart lists instead:',
144
+ ' <!-- list: {"id":"x","style":"cards","items":[{"icon":"book","title":"T","description":"D","action":"msg"}]} -->',
145
+ '- Styles: "cards" (rich cards with icons), "numbered" (ordered steps), "checklist" (checkable items), "compact" (dense).',
146
+ '- Items with "action" are clickable and send the action text as a message.',
147
+ '',
148
+ 'PROGRESS BARS (REQUIRED to show lesson progress):',
149
+ '- Include a progress bar in EVERY teaching response to show where the user is in the lesson:',
150
+ ' <!-- progress: {"id":"lesson-progress","label":"Lesson Progress","current":1,"total":7,"style":"bar"} -->',
151
+ '- Styles: "bar" (horizontal), "steps" (step indicators), "ring" (circular).',
152
+ '- Update the current value as the user progresses through steps.',
153
+ '',
154
+ 'INFO CARDS (REQUIRED for tips, warnings, and key concepts):',
155
+ '- Use info cards for important callouts — NEVER use plain bold text for tips or warnings:',
156
+ ' <!-- card: {"id":"x","type":"tip","title":"Pro Tip","content":"markdown text here"} -->',
157
+ '- Types: "tip" (blue), "warning" (yellow), "error" (red), "success" (green), "concept" (purple).',
158
+ '- Use "concept" for key definitions, "tip" for best practices, "warning" for common mistakes.',
159
+ '',
160
+ 'CODE SNIPPETS (use when showing code examples):',
161
+ ' <!-- code: {"id":"x","language":"js","filename":"app.js","code":"const x = 1;","highlight":[1]} -->',
162
+ '',
163
+ 'STEP TRACKERS (use to show multi-step workflows):',
164
+ ' <!-- steps: {"id":"x","current":2,"steps":[{"label":"Create file","status":"done"},{"label":"Write code","status":"active"},{"label":"Test","status":"pending"}]} -->',
165
+ ' Statuses: done, active, pending.',
166
+ '',
167
+ 'CANVAS VISUAL COMMANDS (appear in the canvas pane, not in the chat bubble):',
168
+ '- Diagrams: <!-- canvas:diagram: {"format":"mermaid","content":"graph TD\\n A-->B","animate":true} -->',
169
+ ' Or use a fenced ```mermaid code block — it auto-routes to the canvas.',
170
+ '- Celebrations: <!-- canvas:celebrate: {"type":"xp","xpAwarded":20} -->',
171
+ ' Types: "xp" (confetti), "level-up" (include "newBelt":"green"), "perfect-score".',
172
+ '- Rich HTML: <!-- canvas:html: {"html":"<div>...</div>"} -->',
173
+ ' Use for images, YouTube embeds, further-reading cards.',
174
+ '- Practice terminal: <!-- canvas:terminal: {"task":"Create a git repo","validations":[{"label":"Repo created","passed":false}],"hints":["Use git init"]} -->',
175
+ '- Dashboard: <!-- canvas:dashboard: {"progress":{"xp":100,"belt":"yellow","modules":[...]}} -->',
176
+ '- Web embed: <!-- canvas:web-embed: {"url":"https://...","title":"Reference"} -->',
177
+ '- Code playground: <!-- canvas:code: {"language":"javascript","code":"function add(a,b) {...}","tests":[...]} -->',
178
+ '- These comments are stripped from the displayed text — the user sees only the rendered component.',
179
+ '- For interactive quizzes and games that need the user\'s answer back, use curl with await (read the skill for details).',
180
+ '',
181
+ 'COMPONENT ORDER (follow this layout in every teaching response):',
182
+ '1. Step tracker / Progress bar — orientation elements go FIRST',
183
+ '2. Teaching text, concept cards, lists, diagrams, code snippets — content in the middle',
184
+ '3. Question + buttons — interactive prompt goes LAST (just before suggestions)',
185
+ '4. Suggestion chips — always the very last element',
186
+ 'Never place a step tracker or progress bar after a question or buttons.',
187
+ '',
188
+ 'EXAMPLE TEACHING RESPONSE (follow this pattern):',
189
+ '```',
190
+ '<!-- progress: {"id":"p1","label":"Lesson Progress","current":2,"total":5,"style":"bar"} -->',
191
+ '',
192
+ 'Great question! Let\'s explore **variables** in JavaScript.',
193
+ '',
194
+ '<!-- card: {"id":"concept1","type":"concept","title":"What is a Variable?","content":"A variable is a named container that stores a value. Think of it as a labeled box."} -->',
195
+ '',
196
+ 'There are three ways to declare variables:',
197
+ '',
198
+ '<!-- list: {"id":"var-types","style":"cards","items":[{"icon":"📦","title":"const","description":"Cannot be reassigned. Use by default."},{"icon":"🔄","title":"let","description":"Can be reassigned. Use for values that change."},{"icon":"⚠️","title":"var","description":"Old syntax. Avoid in modern code."}]} -->',
199
+ '',
200
+ '<!-- card: {"id":"tip1","type":"tip","title":"Best Practice","content":"Always start with `const`. Only switch to `let` if you need to reassign."} -->',
201
+ '',
202
+ '<!-- canvas:diagram: {"format":"mermaid","content":"graph LR\\n const[const] -->|immutable| safe[Safe default]\\n let[let] -->|mutable| change[Values that change]\\n var[var] -->|avoid| legacy[Legacy code]"} -->',
203
+ '',
204
+ 'Which declaration would you use for a user\'s age that changes every year?',
205
+ '',
206
+ '<!-- buttons: {"id":"q1","type":"single","prompt":"Pick the best option:","options":[{"label":"const","value":"I would use const for age"},{"label":"let","value":"I would use let for age"},{"label":"var","value":"I would use var for age"}]} -->',
207
+ '',
208
+ '<!-- suggestions: [{"label":"Show me an example","text":"Show me a code example"},{"label":"Quiz me","text":"Give me a quiz"}] -->',
209
+ '```',
210
+ '',
211
+ 'START: /teach <topic> for lessons, /learn <topic> for free-form learning.',
212
+ ].join('\n'),
213
+ tools: ['Bash(*)', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Skill'],
214
+ permissionMode: 'bypassPermissions',
215
+ settingSources: ['user', 'project'],
216
+ };
91
217
  if (options.pluginDir) {
92
218
  agentConfig.plugins = [{ type: 'local', path: options.pluginDir }];
93
219
  }
220
+ const autoRoutedMediaUrls = new Set();
221
+ const sessionTitleSet = new Set();
222
+
94
223
  const agentServer = createAgentServer({
95
224
  basePath: '/api',
96
225
  config: agentConfig,
97
226
  hooks: {
98
- // Relay chat envelopes to visual WS clients so the canvas receives responses
99
227
  onMessage: (envelope) => {
228
+ if (envelope.type === 'chat:assistant' && envelope.payload?.text) {
229
+ // Auto-enhance plain markdown with smart components when the AI
230
+ // didn't emit them. Must run before extraction so the injected
231
+ // HTML comments get picked up by extractButtons/extractInlineBlocks.
232
+ envelope.payload.text = enhanceWithSmartComponents(envelope.payload.text);
233
+
234
+ const { cleanText, buttons } = extractButtons(envelope.payload.text);
235
+ if (buttons.length > 0) {
236
+ envelope.payload.text = cleanText;
237
+ envelope.payload.buttons = buttons;
238
+ }
239
+ const { cleanText: text2, blocks } = extractInlineBlocks(envelope.payload.text);
240
+ if (blocks.length > 0) {
241
+ envelope.payload.text = text2;
242
+ envelope.payload.blocks = blocks;
243
+ }
244
+ }
245
+
246
+ if (envelope.type === 'chat:assistant' && envelope.payload?.text) {
247
+ const rawText = envelope.payload.text;
248
+ extractAndRouteVisuals(rawText, tierManager, router);
249
+ extractAndRouteMedia(rawText, router, autoRoutedMediaUrls);
250
+ extractAndRouteSuggestions(rawText, tierManager);
251
+ envelope.payload.text = stripCanvasCommands(envelope.payload.text);
252
+ }
253
+
100
254
  const data = JSON.stringify(envelope);
101
255
  tierManager.broadcastWs(data);
102
256
  tierManager.broadcastSse(data);
257
+
258
+ // Persist messages to session store
259
+ const sid = envelope.sessionId || envelope.payload?.sessionId;
260
+ if (sid) {
261
+ try {
262
+ if (envelope.type === 'chat:assistant' && envelope.payload?.text) {
263
+ appendStoredMessage(sid, {
264
+ role: 'assistant',
265
+ text: envelope.payload.text,
266
+ timestamp: envelope.timestamp || Date.now(),
267
+ });
268
+ // Auto-title: set title from first assistant response
269
+ if (!sessionTitleSet.has(sid)) {
270
+ sessionTitleSet.add(sid);
271
+ const title = generateTitle(envelope.payload.text);
272
+ updateStoredMeta(sid, { title });
273
+ }
274
+ } else if (envelope.type === 'chat:tool-use') {
275
+ appendStoredMessage(sid, {
276
+ role: 'tool-use',
277
+ text: `Using ${envelope.payload?.toolName || 'tool'}`,
278
+ data: envelope.payload,
279
+ timestamp: envelope.timestamp || Date.now(),
280
+ });
281
+ } else if (envelope.type === 'chat:tool-result') {
282
+ appendStoredMessage(sid, {
283
+ role: 'tool-result',
284
+ data: envelope.payload,
285
+ timestamp: envelope.timestamp || Date.now(),
286
+ });
287
+ }
288
+ } catch { /* don't break chat flow on storage errors */ }
289
+ }
103
290
  },
104
291
  },
105
292
  });
106
293
  const chatMiddleware = agentServer.middleware();
107
294
 
295
+ // Patch: escape leading '/' in messages to prevent SDK slash command interception.
296
+ // The Claude Agent SDK intercepts messages starting with '/' as built-in slash commands
297
+ // (e.g., /debug, /review). Our custom skills (/teach, /learn, etc.) aren't registered
298
+ // with the SDK, so they get "Unknown skill" errors. Prepending a zero-width space
299
+ // bypasses the SDK's slash handler while preserving the text for Claude.
300
+ const sm = agentServer.sessions;
301
+ const origSend = sm.sendMessage.bind(sm);
302
+ sm.sendMessage = function(sessionId, text) {
303
+ // Persist user message + auto-tag session
304
+ try {
305
+ appendStoredMessage(sessionId, {
306
+ role: 'user',
307
+ text,
308
+ timestamp: Date.now(),
309
+ });
310
+ const tag = detectSessionTag(text);
311
+ if (tag) {
312
+ updateStoredMeta(sessionId, tag);
313
+ sessionTitleSet.add(sessionId);
314
+ }
315
+ } catch { /* don't break chat flow */ }
316
+
317
+ const escaped = text.startsWith('/') ? '\u200B' + text : text;
318
+ return origSend(sessionId, escaped);
319
+ };
320
+
108
321
  if (options.onTierChange) {
109
322
  tierManager.onTierChange(options.onTierChange);
110
323
  }
@@ -114,13 +327,14 @@ export function startServer(options = {}) {
114
327
 
115
328
  // Create HTTP server
116
329
  const server = createServer((req, res) => {
117
- handleRequest(req, res, tierManager, router, options, chatMiddleware);
330
+ handleRequest(req, res, tierManager, router, options, chatMiddleware, agentServer);
118
331
  });
119
332
 
120
333
  // Both WSSes use noServer to avoid ws library destroying sockets
121
334
  // on path mismatch (ws docs: use noServer for multiple WSSes)
122
335
  const wss = new WebSocketServer({ noServer: true });
123
336
  const chatWss = new WebSocketServer({ noServer: true });
337
+ const ptyWss = new WebSocketServer({ noServer: true });
124
338
 
125
339
  server.on('upgrade', (req, socket, head) => {
126
340
  const { pathname } = new URL(req.url, `http://${req.headers.host}`);
@@ -132,11 +346,60 @@ export function startServer(options = {}) {
132
346
  chatWss.handleUpgrade(req, socket, head, (ws) => {
133
347
  chatWss.emit('connection', ws, req);
134
348
  });
349
+ } else if (pathname.startsWith('/ws/terminal/')) {
350
+ ptyWss.handleUpgrade(req, socket, head, (ws) => {
351
+ const sessionId = pathname.replace('/ws/terminal/', '');
352
+ ptyWss.emit('connection', ws, sessionId);
353
+ });
135
354
  } else {
136
355
  socket.destroy();
137
356
  }
138
357
  });
139
358
 
359
+ // PTY terminal WebSocket connections
360
+ ptyWss.on('connection', (ws, sessionId) => {
361
+ if (!hasPtySession(sessionId)) {
362
+ ws.close(4004, 'Unknown PTY session');
363
+ return;
364
+ }
365
+
366
+ setPtyCallbacks(sessionId,
367
+ (data) => {
368
+ if (ws.readyState === 1) {
369
+ ws.send(JSON.stringify({ type: 'data', data }));
370
+ }
371
+ // Tee output to agent event queue for observation
372
+ router.enqueuePtyEvent(sessionId, data);
373
+ },
374
+ ({ exitCode, signal }) => {
375
+ if (ws.readyState === 1) {
376
+ ws.send(JSON.stringify({ type: 'exit', exitCode, signal }));
377
+ ws.close(1000, 'PTY exited');
378
+ }
379
+ }
380
+ );
381
+
382
+ ws.on('message', (rawData) => {
383
+ try {
384
+ const msg = JSON.parse(rawData.toString());
385
+ switch (msg.type) {
386
+ case 'input':
387
+ writeToPty(sessionId, msg.data);
388
+ break;
389
+ case 'resize':
390
+ if (msg.cols && msg.rows) {
391
+ resizePty(sessionId, msg.cols, msg.rows);
392
+ }
393
+ break;
394
+ }
395
+ } catch { /* ignore malformed messages */ }
396
+ });
397
+
398
+ ws.on('close', () => {
399
+ // Keep PTY alive for reconnect; cleanup timer handles idle sessions
400
+ });
401
+ });
402
+
140
403
  // Wire agent-web transport to the chat WSS
141
404
  chatWss.on('connection', (ws) => {
142
405
  agentServer.transport.handleWsConnection(
@@ -257,11 +520,14 @@ export function startServer(options = {}) {
257
520
  tierManager.startHeartbeat();
258
521
 
259
522
  // Periodically clean up idle terminal sessions (every 60s)
260
- const terminalCleanupInterval = setInterval(() => cleanupIdleSessions(), 60000);
523
+ const terminalCleanupInterval = setInterval(() => {
524
+ cleanupIdleSessions();
525
+ cleanupIdlePtySessions();
526
+ }, 60000);
261
527
 
262
528
  // Start listening
263
529
  server.listen(port, () => {
264
- console.log(`ClaudeTeach bridge server running on http://localhost:${port}`);
530
+ console.log(`Socrates bridge server running on http://localhost:${port}`);
265
531
  console.log(` WebSocket /ws — bidirectional (extension + canvas)`);
266
532
  console.log(` SSE /sse — server-push (canvas-only)`);
267
533
  console.log(` POST /api/event — receive user events`);
@@ -270,9 +536,13 @@ export function startServer(options = {}) {
270
536
  console.log(` GET /api/progress — progress data`);
271
537
  console.log(` GET /api/events/wait — long-poll for events`);
272
538
  console.log(` POST /api/terminal/exec — execute terminal commands`);
273
- console.log(` POST /api/quiz send quiz + wait for answer`);
274
- console.log(` POST /api/gamesend game + wait for result`);
539
+ console.log(` POST /api/canvas unified visual commands (with optional await)`);
540
+ console.log(` POST /api/pty/createcreate PTY terminal session`);
541
+ console.log(` WebSocket /ws/terminal/:id — PTY terminal I/O`);
542
+ console.log(` POST /api/playground/exec — execute code snippets`);
543
+ console.log(` POST /api/workshop/setup — set up workshop scenario`);
275
544
  console.log(` REST /api/chat/* — chat (via @shaykec/agent-web)`);
545
+ console.log(` REST /api/sessions — session management (list/get/update/delete/purge)`);
276
546
  console.log(` WebSocket /api/ws — chat WebSocket`);
277
547
  console.log(` SSE /api/sse — chat SSE`);
278
548
  console.log(` GET /health — health check`);
@@ -301,10 +571,10 @@ export function startServer(options = {}) {
301
571
  /**
302
572
  * Handle HTTP requests.
303
573
  */
304
- function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
574
+ function handleRequest(req, res, tierManager, router, options, chatMiddleware, agentServer) {
305
575
  // CORS headers
306
576
  res.setHeader('Access-Control-Allow-Origin', '*');
307
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
577
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
308
578
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
309
579
 
310
580
  if (req.method === 'OPTIONS') {
@@ -318,7 +588,15 @@ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
318
588
 
319
589
  // --- Chat endpoints delegated to @shaykec/agent-web ---
320
590
  if (pathname.startsWith('/api/chat') || pathname === '/api/sse' || pathname === '/api/health') {
321
- chatMiddleware(req, res);
591
+ try {
592
+ chatMiddleware(req, res);
593
+ } catch (err) {
594
+ console.error('[bridge] chatMiddleware sync error:', err.message);
595
+ if (!res.headersSent) {
596
+ res.writeHead(500, { 'Content-Type': 'application/json' });
597
+ res.end(JSON.stringify({ error: 'Internal server error' }));
598
+ }
599
+ }
322
600
  return;
323
601
  }
324
602
 
@@ -349,6 +627,11 @@ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
349
627
  return;
350
628
  }
351
629
 
630
+ if (req.method === 'GET' && pathname === '/api/constants') {
631
+ sendJson(res, 200, { belts: BELTS });
632
+ return;
633
+ }
634
+
352
635
  if (req.method === 'GET' && pathname === '/api/events') {
353
636
  handleApiPollEvents(res, router);
354
637
  return;
@@ -364,8 +647,8 @@ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
364
647
  return;
365
648
  }
366
649
 
367
- if (req.method === 'POST' && pathname === '/api/visual') {
368
- handleApiVisual(req, res, router);
650
+ if (req.method === 'POST' && pathname === '/api/canvas') {
651
+ handleApiCanvas(req, res, router, url);
369
652
  return;
370
653
  }
371
654
 
@@ -374,13 +657,18 @@ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
374
657
  return;
375
658
  }
376
659
 
377
- if (req.method === 'POST' && pathname === '/api/quiz') {
378
- handleApiQuiz(req, res, router, url);
660
+ if (req.method === 'POST' && pathname === '/api/pty/create') {
661
+ handleApiPtyCreate(req, res);
662
+ return;
663
+ }
664
+
665
+ if (req.method === 'POST' && pathname === '/api/playground/exec') {
666
+ handleApiPlaygroundExec(req, res);
379
667
  return;
380
668
  }
381
669
 
382
- if (req.method === 'POST' && pathname === '/api/game') {
383
- handleApiGame(req, res, router, url);
670
+ if (req.method === 'POST' && pathname === '/api/workshop/setup') {
671
+ handleApiWorkshopSetup(req, res, router);
384
672
  return;
385
673
  }
386
674
 
@@ -389,12 +677,77 @@ function handleRequest(req, res, tierManager, router, options, chatMiddleware) {
389
677
  return;
390
678
  }
391
679
 
680
+ // --- Progress write ---
681
+ if (req.method === 'POST' && pathname === '/api/progress') {
682
+ handleApiProgressWrite(req, res, options, router);
683
+ return;
684
+ }
685
+
686
+ // --- Module static resources (images, videos, HTML snippets) ---
687
+ {
688
+ const resMatch = pathname.match(/^\/api\/module\/([\w-]+)\/resources\/([\w.-]+)$/);
689
+ if (req.method === 'GET' && resMatch) {
690
+ handleApiModuleResource(res, resMatch[1], resMatch[2]);
691
+ return;
692
+ }
693
+ }
694
+
695
+ // --- Module content ---
696
+ if (req.method === 'GET' && pathname.startsWith('/api/module/')) {
697
+ const slug = pathname.replace('/api/module/', '');
698
+ if (slug && !slug.includes('/')) {
699
+ handleApiModuleContent(res, slug);
700
+ return;
701
+ }
702
+ }
703
+
392
704
  // --- Modules catalog ---
393
705
  if (req.method === 'GET' && pathname === '/api/modules') {
394
706
  handleApiModules(res);
395
707
  return;
396
708
  }
397
709
 
710
+ // --- Journeys ---
711
+ if (req.method === 'GET' && pathname === '/api/journeys') {
712
+ handleApiJourneys(res, options);
713
+ return;
714
+ }
715
+
716
+ if (req.method === 'POST' && pathname === '/api/journeys/activate') {
717
+ handleApiJourneyActivate(req, res, options);
718
+ return;
719
+ }
720
+
721
+ // --- Sessions ---
722
+ if (req.method === 'GET' && pathname === '/api/sessions') {
723
+ handleApiSessionsList(res);
724
+ return;
725
+ }
726
+
727
+ {
728
+ const sessionMatch = pathname.match(/^\/api\/sessions\/([a-f0-9-]+)$/);
729
+ if (sessionMatch) {
730
+ const sid = sessionMatch[1];
731
+ if (req.method === 'GET') {
732
+ handleApiSessionGet(res, sid);
733
+ return;
734
+ }
735
+ if (req.method === 'PUT') {
736
+ handleApiSessionUpdate(req, res, sid);
737
+ return;
738
+ }
739
+ if (req.method === 'DELETE') {
740
+ handleApiSessionDelete(res, sid, agentServer);
741
+ return;
742
+ }
743
+ }
744
+ }
745
+
746
+ if (req.method === 'DELETE' && pathname === '/api/sessions') {
747
+ handleApiSessionsPurge(res, agentServer);
748
+ return;
749
+ }
750
+
398
751
  // --- Settings ---
399
752
  if (req.method === 'GET' && pathname === '/api/settings') {
400
753
  handleApiSettingsGet(res);
@@ -516,6 +869,7 @@ function handleApiCapture(req, res) {
516
869
 
517
870
  /**
518
871
  * GET /api/progress — return progress data with achievements.
872
+ * Enriches module entries with title/description/difficulty from module.yaml catalog.
519
873
  */
520
874
  function handleApiProgress(res, options) {
521
875
  let progress;
@@ -525,6 +879,11 @@ function handleApiProgress(res, options) {
525
879
  progress = { user: { xp: 0 }, modules: {} };
526
880
  }
527
881
 
882
+ // Enrich modules with catalog metadata
883
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
884
+ const moduleMap = buildModuleMap(modulesDir);
885
+ progress.modules = enrichModulesWithMetadata(progress.modules, moduleMap);
886
+
528
887
  // Include achievements data from progress file
529
888
  const achievements = progress.achievements || { unlocked: [], progress: {} };
530
889
  const unlocked = (achievements.unlocked || []).map(a => ({
@@ -590,6 +949,7 @@ function handleApiModules(res) {
590
949
  }
591
950
  }
592
951
  if (meta.slug) {
952
+ const hasWorkshop = existsSync(join(modulesDir, entry, 'workshop.yaml'));
593
953
  modules.push({
594
954
  slug: meta.slug,
595
955
  title: meta.title || meta.slug,
@@ -597,6 +957,7 @@ function handleApiModules(res) {
597
957
  category: meta.category || 'general',
598
958
  difficulty: meta.difficulty || 'beginner',
599
959
  icon: meta.icon || null,
960
+ hasWorkshop,
600
961
  });
601
962
  }
602
963
  } catch {
@@ -612,11 +973,565 @@ function handleApiModules(res) {
612
973
  }
613
974
 
614
975
  /**
615
- * Settings file path: ~/.claude-teach/config.yaml
976
+ * GET /api/journeys return all journeys with resolved module status and progress.
977
+ */
978
+ function handleApiJourneys(res, options) {
979
+ try {
980
+ const journeysDir = resolve(__dirname, '..', '..', 'journeys');
981
+ const journeys = [];
982
+
983
+ if (existsSync(journeysDir)) {
984
+ const files = readdirSync(journeysDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
985
+ for (const file of files) {
986
+ try {
987
+ const raw = readFileSync(join(journeysDir, file), 'utf-8');
988
+ const parsed = parseYamlSimple(raw);
989
+ if (parsed && parsed.slug) {
990
+ journeys.push(parsed);
991
+ }
992
+ } catch { /* skip */ }
993
+ }
994
+ }
995
+
996
+ let progress = { modules: {}, journey: { active: null, progress: {} } };
997
+ if (options.progressProvider && typeof options.progressProvider.getProgress === 'function') {
998
+ progress = options.progressProvider.getProgress();
999
+ }
1000
+
1001
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
1002
+ const moduleMap = buildModuleMap(modulesDir);
1003
+
1004
+ const resolved = journeys.map(j => resolveJourneyForApi(j, moduleMap, progress));
1005
+ sendJson(res, 200, {
1006
+ journeys: resolved,
1007
+ activeJourney: progress.journey?.active || null,
1008
+ });
1009
+ } catch {
1010
+ sendJson(res, 200, { journeys: [], activeJourney: null });
1011
+ }
1012
+ }
1013
+
1014
+ /**
1015
+ * POST /api/journeys/activate — set or clear the active journey.
1016
+ */
1017
+ function handleApiJourneyActivate(req, res, options) {
1018
+ readBody(req, (err, body) => {
1019
+ if (err) return sendJson(res, 400, { error: 'Invalid request body' });
1020
+ try {
1021
+ const { slug } = body;
1022
+ if (options.progressProvider && typeof options.progressProvider.getProgress === 'function') {
1023
+ const progress = options.progressProvider.getProgress();
1024
+ if (!progress.journey) progress.journey = { active: null, progress: {} };
1025
+
1026
+ if (slug) {
1027
+ progress.journey.active = slug;
1028
+ if (!progress.journey.progress[slug]) {
1029
+ progress.journey.progress[slug] = { started: new Date().toISOString().split('T')[0] };
1030
+ }
1031
+ } else {
1032
+ progress.journey.active = null;
1033
+ }
1034
+
1035
+ if (typeof options.progressProvider.saveProgress === 'function') {
1036
+ options.progressProvider.saveProgress(progress);
1037
+ }
1038
+ sendJson(res, 200, { ok: true, active: progress.journey.active });
1039
+ } else {
1040
+ sendJson(res, 200, { ok: true, active: null });
1041
+ }
1042
+ } catch (activateErr) {
1043
+ sendJson(res, 500, { error: activateErr.message });
1044
+ }
1045
+ });
1046
+ }
1047
+
1048
+ /**
1049
+ * Build a map of module slugs to basic metadata from the modules directory.
1050
+ */
1051
+ function buildModuleMap(modulesDir) {
1052
+ const map = new Map();
1053
+ try {
1054
+ if (!existsSync(modulesDir)) return map;
1055
+ for (const entry of readdirSync(modulesDir)) {
1056
+ const yamlPath = join(modulesDir, entry, 'module.yaml');
1057
+ if (!existsSync(yamlPath)) continue;
1058
+ try {
1059
+ const raw = readFileSync(yamlPath, 'utf-8');
1060
+ const meta = parseYamlSimple(raw);
1061
+ if (meta?.slug) {
1062
+ map.set(meta.slug, {
1063
+ slug: meta.slug,
1064
+ title: meta.title || meta.slug,
1065
+ description: meta.description || '',
1066
+ difficulty: meta.difficulty || 'beginner',
1067
+ });
1068
+ }
1069
+ } catch { /* skip */ }
1070
+ }
1071
+ } catch { /* skip */ }
1072
+ return map;
1073
+ }
1074
+
1075
+ /**
1076
+ * Parse simple flat YAML fields (no nested structures).
1077
+ * For journey files we need nested parsing, so use JSON-safe subset.
1078
+ */
1079
+ function parseYamlSimple(raw) {
1080
+ try {
1081
+ // Use dynamic import workaround — js-yaml may not be available in bridge
1082
+ // Instead, parse the YAML structure manually for journeys
1083
+ const lines = raw.split('\n');
1084
+ const result = {};
1085
+ let currentKey = null;
1086
+ let currentArray = null;
1087
+ let inStages = false;
1088
+ let currentStage = null;
1089
+ let inModules = false;
1090
+
1091
+ for (const line of lines) {
1092
+ const trimmed = line.trimEnd();
1093
+
1094
+ // Top-level scalar
1095
+ const scalarMatch = trimmed.match(/^(\w[\w_-]*):\s*(.+)$/);
1096
+ if (scalarMatch && !inStages) {
1097
+ currentKey = scalarMatch[1];
1098
+ let val = scalarMatch[2].trim();
1099
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
1100
+ if (val.startsWith('[') && val.endsWith(']')) {
1101
+ val = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
1102
+ } else if (/^\d+$/.test(val)) {
1103
+ val = parseInt(val, 10);
1104
+ }
1105
+ result[currentKey] = val;
1106
+ continue;
1107
+ }
1108
+
1109
+ // Top-level key with no value (start of block)
1110
+ const blockMatch = trimmed.match(/^(\w[\w_-]*):\s*$/);
1111
+ if (blockMatch) {
1112
+ currentKey = blockMatch[1];
1113
+ if (currentKey === 'stages') {
1114
+ inStages = true;
1115
+ result.stages = [];
1116
+ }
1117
+ continue;
1118
+ }
1119
+
1120
+ if (inStages) {
1121
+ // New stage entry
1122
+ const stageStart = trimmed.match(/^\s+-\s+name:\s*"?(.+?)"?\s*$/);
1123
+ if (stageStart) {
1124
+ currentStage = { name: stageStart[1], description: '', modules: [] };
1125
+ result.stages.push(currentStage);
1126
+ inModules = false;
1127
+ continue;
1128
+ }
1129
+
1130
+ // Stage description
1131
+ const descMatch = trimmed.match(/^\s+description:\s*"?(.+?)"?\s*$/);
1132
+ if (descMatch && currentStage) {
1133
+ currentStage.description = descMatch[1];
1134
+ continue;
1135
+ }
1136
+
1137
+ // Modules block start
1138
+ if (trimmed.match(/^\s+modules:\s*$/)) {
1139
+ inModules = true;
1140
+ continue;
1141
+ }
1142
+
1143
+ // Module entry — object form { slug: x, title: y }
1144
+ const objModule = trimmed.match(/^\s+-\s*\{\s*slug:\s*(\S+),\s*title:\s*"(.+?)"\s*\}/);
1145
+ if (objModule && inModules && currentStage) {
1146
+ currentStage.modules.push({ slug: objModule[1], title: objModule[2] });
1147
+ continue;
1148
+ }
1149
+
1150
+ // Module entry — plain slug
1151
+ const slugModule = trimmed.match(/^\s+-\s+(\S+)\s*$/);
1152
+ if (slugModule && inModules && currentStage) {
1153
+ currentStage.modules.push(slugModule[1]);
1154
+ continue;
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ return result;
1160
+ } catch {
1161
+ return null;
1162
+ }
1163
+ }
1164
+
1165
+ /**
1166
+ * Resolve a journey's modules against the module map and progress.
1167
+ */
1168
+ function resolveJourneyForApi(journey, moduleMap, progress) {
1169
+ const moduleProgress = progress.modules || {};
1170
+ let totalModules = 0;
1171
+ let completedModules = 0;
1172
+ let comingSoonModules = 0;
1173
+
1174
+ const stages = (journey.stages || []).map((stage, idx) => {
1175
+ const modules = (stage.modules || []).map(ref => {
1176
+ totalModules++;
1177
+ const slug = typeof ref === 'string' ? ref : ref.slug;
1178
+ const titleHint = typeof ref === 'object' ? ref.title : null;
1179
+ const regMod = moduleMap.get(slug);
1180
+ const modProg = moduleProgress[slug];
1181
+
1182
+ if (!regMod) {
1183
+ comingSoonModules++;
1184
+ return { slug, title: titleHint || slug, status: 'coming_soon' };
1185
+ }
1186
+
1187
+ let status = 'available';
1188
+ if (modProg?.status === 'completed') { status = 'completed'; completedModules++; }
1189
+ else if (modProg?.status === 'in-progress') { status = 'in_progress'; }
1190
+
1191
+ return { slug, title: regMod.title, status, difficulty: regMod.difficulty };
1192
+ });
1193
+
1194
+ const existing = modules.filter(m => m.status !== 'coming_soon');
1195
+ const done = existing.length > 0 && existing.every(m => m.status === 'completed');
1196
+ const inProg = modules.some(m => m.status === 'in_progress' || m.status === 'completed') && !done;
1197
+
1198
+ let stageStatus = 'locked';
1199
+ if (done) stageStatus = 'completed';
1200
+ else if (inProg) stageStatus = 'in_progress';
1201
+
1202
+ return { name: stage.name, description: stage.description || '', stageIndex: idx, status: stageStatus, modules };
1203
+ });
1204
+
1205
+ // Unlock logic
1206
+ for (let i = 0; i < stages.length; i++) {
1207
+ if (i === 0 && stages[i].status === 'locked') {
1208
+ stages[i].status = 'available';
1209
+ stages[i].modules.forEach(m => { if (m.status !== 'coming_soon' && m.status !== 'completed' && m.status !== 'in_progress') m.status = 'available'; });
1210
+ } else if (i > 0 && stages[i - 1].status === 'completed' && stages[i].status === 'locked') {
1211
+ stages[i].status = 'available';
1212
+ stages[i].modules.forEach(m => { if (m.status !== 'coming_soon' && m.status !== 'completed' && m.status !== 'in_progress') m.status = 'available'; });
1213
+ }
1214
+ }
1215
+
1216
+ const currentStageIndex = stages.findIndex(s => s.status !== 'completed');
1217
+ const availableModules = totalModules - comingSoonModules;
1218
+
1219
+ return {
1220
+ slug: journey.slug,
1221
+ title: journey.title,
1222
+ description: journey.description || '',
1223
+ icon: journey.icon || 'book',
1224
+ tags: Array.isArray(journey.tags) ? journey.tags : [],
1225
+ difficulty: journey.difficulty || 'beginner-to-advanced',
1226
+ estimatedHours: journey.estimated_hours || null,
1227
+ stages,
1228
+ stats: {
1229
+ totalModules,
1230
+ completedModules,
1231
+ comingSoonModules,
1232
+ availableModules,
1233
+ totalStages: stages.length,
1234
+ completedStages: stages.filter(s => s.status === 'completed').length,
1235
+ currentStageIndex: currentStageIndex >= 0 ? currentStageIndex : stages.length - 1,
1236
+ progressPercent: availableModules > 0 ? Math.round((completedModules / availableModules) * 100) : 0,
1237
+ },
1238
+ };
1239
+ }
1240
+
1241
+ /**
1242
+ * POST /api/progress — write progress updates.
1243
+ * Accepts { xp, module, status, walkthroughStep } and merges into the progress file.
1244
+ * Uses progressProvider.saveProgress() when available for proper YAML serialization.
1245
+ * Broadcasts updated progress to all connected canvas clients.
1246
+ */
1247
+ function handleApiProgressWrite(req, res, options, router) {
1248
+ readBody(req, (err, body) => {
1249
+ if (err) {
1250
+ sendJson(res, 400, { error: 'Invalid request body' });
1251
+ return;
1252
+ }
1253
+
1254
+ if (!body || typeof body !== 'object') {
1255
+ sendJson(res, 400, { error: 'Body must be a JSON object' });
1256
+ return;
1257
+ }
1258
+
1259
+ const provider = options.progressProvider;
1260
+ const hasProvider = provider
1261
+ && typeof provider.getProgress === 'function'
1262
+ && typeof provider.saveProgress === 'function';
1263
+
1264
+ try {
1265
+ const progress = hasProvider
1266
+ ? provider.getProgress()
1267
+ : { user: { xp: 0 }, modules: {} };
1268
+
1269
+ if (!progress.user) progress.user = { xp: 0 };
1270
+ if (!progress.modules) progress.modules = {};
1271
+
1272
+ if (typeof body.xp === 'number') {
1273
+ progress.user.xp = body.xp;
1274
+ }
1275
+
1276
+ if (body.module && body.status) {
1277
+ if (!progress.modules[body.module]) {
1278
+ progress.modules[body.module] = {
1279
+ status: body.status,
1280
+ started: new Date().toISOString().split('T')[0],
1281
+ };
1282
+ } else {
1283
+ progress.modules[body.module].status = body.status;
1284
+ }
1285
+ progress.modules[body.module].last_session = new Date().toISOString().split('T')[0];
1286
+
1287
+ if (typeof body.walkthroughStep === 'number') {
1288
+ progress.modules[body.module].walkthrough_step = body.walkthroughStep;
1289
+ }
1290
+ }
1291
+
1292
+ progress.user.modules_completed = Object.values(progress.modules)
1293
+ .filter(m => m.status === 'completed').length;
1294
+
1295
+ if (hasProvider) {
1296
+ provider.saveProgress(progress);
1297
+ }
1298
+
1299
+ // Broadcast updated progress to connected canvas clients (enriched with catalog metadata)
1300
+ if (router) {
1301
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
1302
+ const moduleMap = buildModuleMap(modulesDir);
1303
+ const enriched = enrichModulesWithMetadata(progress.modules, moduleMap);
1304
+ const modules = Object.entries(enriched).map(([slug, m]) => ({ slug, ...m }));
1305
+ const dashboardEnvelope = createEnvelope('canvas:dashboard', {
1306
+ progress: {
1307
+ xp: progress.user.xp || 0,
1308
+ belt: progress.user.belt,
1309
+ modules,
1310
+ completedCount: modules.filter(m => m.status === 'completed').length,
1311
+ totalCount: modules.length,
1312
+ },
1313
+ }, 'bridge');
1314
+ router.routeVisualCommand(dashboardEnvelope);
1315
+ }
1316
+
1317
+ sendJson(res, 200, { ok: true });
1318
+ } catch (writeErr) {
1319
+ sendJson(res, 500, { error: writeErr.message });
1320
+ }
1321
+ });
1322
+ }
1323
+
1324
+ /**
1325
+ * GET /api/module/:slug — serve full module content.
1326
+ */
1327
+ function handleApiModuleContent(res, slug) {
1328
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
1329
+ const moduleDir = join(modulesDir, slug);
1330
+
1331
+ if (!existsSync(join(moduleDir, 'module.yaml'))) {
1332
+ sendJson(res, 404, { error: `Module "${slug}" not found` });
1333
+ return;
1334
+ }
1335
+
1336
+ try {
1337
+ const files = {};
1338
+ const entries = readdirSync(moduleDir);
1339
+ for (const entry of entries) {
1340
+ const filePath = join(moduleDir, entry);
1341
+ try {
1342
+ const stat = statSync(filePath);
1343
+ if (stat.isFile() && stat.size < 100_000) {
1344
+ files[entry] = readFileSync(filePath, 'utf-8');
1345
+ }
1346
+ } catch { /* skip unreadable files */ }
1347
+ }
1348
+
1349
+ const meta = {};
1350
+ const yamlRaw = files['module.yaml'] || '';
1351
+ for (const line of yamlRaw.split('\n')) {
1352
+ const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
1353
+ if (match) {
1354
+ let val = match[2].trim();
1355
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
1356
+ if (val === 'true') val = true;
1357
+ else if (val === 'false') val = false;
1358
+ else if (/^\d+$/.test(val)) val = parseInt(val, 10);
1359
+ meta[match[1]] = val;
1360
+ }
1361
+ }
1362
+
1363
+ // Include list of static resources if resources/ directory exists
1364
+ const resourcesDir = join(moduleDir, 'resources');
1365
+ let resources = [];
1366
+ try {
1367
+ if (existsSync(resourcesDir)) {
1368
+ resources = readdirSync(resourcesDir).filter(f => {
1369
+ try { return statSync(join(resourcesDir, f)).isFile(); } catch { return false; }
1370
+ });
1371
+ }
1372
+ } catch { /* no resources */ }
1373
+
1374
+ sendJson(res, 200, { slug, meta, files, resources });
1375
+ } catch (readErr) {
1376
+ sendJson(res, 500, { error: readErr.message });
1377
+ }
1378
+ }
1379
+
1380
+ const RESOURCE_CONTENT_TYPES = {
1381
+ '.png': 'image/png',
1382
+ '.jpg': 'image/jpeg',
1383
+ '.jpeg': 'image/jpeg',
1384
+ '.gif': 'image/gif',
1385
+ '.svg': 'image/svg+xml',
1386
+ '.webp': 'image/webp',
1387
+ '.avif': 'image/avif',
1388
+ '.mp4': 'video/mp4',
1389
+ '.webm': 'video/webm',
1390
+ '.ogg': 'video/ogg',
1391
+ '.html': 'text/html',
1392
+ '.css': 'text/css',
1393
+ '.js': 'text/javascript',
1394
+ '.json': 'application/json',
1395
+ };
1396
+ const MAX_RESOURCE_SIZE = 10 * 1024 * 1024; // 10 MB
1397
+
1398
+ /**
1399
+ * GET /api/module/:slug/resources/:filename — serve static resource files.
1400
+ */
1401
+ function handleApiModuleResource(res, slug, filename) {
1402
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
1403
+ sendJson(res, 400, { error: 'Invalid filename' });
1404
+ return;
1405
+ }
1406
+
1407
+ const modulesDir = resolve(__dirname, '..', '..', 'modules');
1408
+ const filePath = join(modulesDir, slug, 'resources', filename);
1409
+
1410
+ if (!existsSync(filePath)) {
1411
+ sendJson(res, 404, { error: 'Resource not found' });
1412
+ return;
1413
+ }
1414
+
1415
+ try {
1416
+ const stat = statSync(filePath);
1417
+ if (!stat.isFile() || stat.size > MAX_RESOURCE_SIZE) {
1418
+ sendJson(res, 400, { error: 'Resource too large or not a file' });
1419
+ return;
1420
+ }
1421
+
1422
+ const ext = '.' + filename.split('.').pop().toLowerCase();
1423
+ const contentType = RESOURCE_CONTENT_TYPES[ext] || 'application/octet-stream';
1424
+
1425
+ res.writeHead(200, {
1426
+ 'Content-Type': contentType,
1427
+ 'Content-Length': stat.size,
1428
+ 'Cache-Control': 'public, max-age=86400',
1429
+ });
1430
+ const stream = createReadStream(filePath);
1431
+ stream.pipe(res);
1432
+ stream.on('error', () => {
1433
+ if (!res.headersSent) sendJson(res, 500, { error: 'Read error' });
1434
+ });
1435
+ } catch (err) {
1436
+ sendJson(res, 500, { error: err.message });
1437
+ }
1438
+ }
1439
+
1440
+ // --- Session management handlers ---
1441
+
1442
+ /**
1443
+ * GET /api/sessions — list all sessions (metadata only).
1444
+ */
1445
+ function handleApiSessionsList(res) {
1446
+ try {
1447
+ const sessions = listStoredSessions();
1448
+ sendJson(res, 200, { sessions });
1449
+ } catch (err) {
1450
+ sendJson(res, 500, { error: err.message });
1451
+ }
1452
+ }
1453
+
1454
+ /**
1455
+ * GET /api/sessions/:id — get full session with messages.
1456
+ */
1457
+ function handleApiSessionGet(res, sessionId) {
1458
+ try {
1459
+ const session = getStoredSession(sessionId);
1460
+ if (!session) {
1461
+ sendJson(res, 404, { error: 'Session not found' });
1462
+ return;
1463
+ }
1464
+ sendJson(res, 200, session);
1465
+ } catch (err) {
1466
+ sendJson(res, 500, { error: err.message });
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * PUT /api/sessions/:id — update session metadata.
1472
+ */
1473
+ function handleApiSessionUpdate(req, res, sessionId) {
1474
+ readBody(req, (err, body) => {
1475
+ if (err) {
1476
+ sendJson(res, 400, { error: 'Invalid request body' });
1477
+ return;
1478
+ }
1479
+ try {
1480
+ const updated = updateStoredMeta(sessionId, body);
1481
+ if (!updated) {
1482
+ sendJson(res, 404, { error: 'Session not found' });
1483
+ return;
1484
+ }
1485
+ sendJson(res, 200, { ok: true, session: {
1486
+ sessionId: updated.sessionId,
1487
+ title: updated.title,
1488
+ module: updated.module,
1489
+ topic: updated.topic,
1490
+ pinned: updated.pinned,
1491
+ }});
1492
+ } catch (err) {
1493
+ sendJson(res, 500, { error: err.message });
1494
+ }
1495
+ });
1496
+ }
1497
+
1498
+ /**
1499
+ * DELETE /api/sessions/:id — delete a single session.
1500
+ */
1501
+ function handleApiSessionDelete(res, sessionId, agentServer) {
1502
+ try {
1503
+ // Close SDK session if active
1504
+ if (agentServer?.sessions) {
1505
+ agentServer.sessions.closeSession(sessionId).catch(() => {});
1506
+ }
1507
+ const deleted = deleteStoredSession(sessionId);
1508
+ sendJson(res, 200, { ok: true, deleted });
1509
+ } catch (err) {
1510
+ sendJson(res, 500, { error: err.message });
1511
+ }
1512
+ }
1513
+
1514
+ /**
1515
+ * DELETE /api/sessions — purge all session history.
1516
+ */
1517
+ function handleApiSessionsPurge(res, agentServer) {
1518
+ try {
1519
+ if (agentServer?.sessions) {
1520
+ agentServer.sessions.closeAll().catch(() => {});
1521
+ }
1522
+ const count = purgeStoredSessions();
1523
+ sendJson(res, 200, { ok: true, purged: count });
1524
+ } catch (err) {
1525
+ sendJson(res, 500, { error: err.message });
1526
+ }
1527
+ }
1528
+
1529
+ /**
1530
+ * Settings file path: ~/.socrates/config.yaml
616
1531
  */
617
1532
  const SETTINGS_DIR = join(
618
1533
  process.env.HOME || process.env.USERPROFILE || '/tmp',
619
- '.claude-teach'
1534
+ '.socrates'
620
1535
  );
621
1536
  const SETTINGS_FILE = join(SETTINGS_DIR, 'config.yaml');
622
1537
 
@@ -625,6 +1540,7 @@ const DEFAULT_SETTINGS = {
625
1540
  accent: 'blue',
626
1541
  chatPosition: 'left',
627
1542
  fontSize: 14,
1543
+ model: 'claude-sonnet-4-6',
628
1544
  animations: { confetti: true, transitions: true, diagrams: true },
629
1545
  };
630
1546
 
@@ -751,65 +1667,18 @@ function handleApiTemplates(res) {
751
1667
  }
752
1668
 
753
1669
  /**
754
- * POST /api/visualpush a visual command to connected clients.
755
- * Used by the plugin to send canvas:* commands.
756
- * REJECTS canvas:quiz messages those MUST go through POST /api/quiz.
757
- */
758
- function handleApiVisual(req, res, router) {
759
- readBody(req, (err, body) => {
760
- if (err) {
761
- sendJson(res, 400, { error: 'Invalid request body' });
762
- return;
763
- }
764
-
765
- const { valid, envelope, error } = parseEnvelope(body);
766
- if (!valid) {
767
- sendJson(res, 400, { error });
768
- return;
769
- }
770
-
771
- // Reject quiz messages — they must use POST /api/quiz to capture answers
772
- if (envelope.type === 'canvas:quiz') {
773
- sendJson(res, 400, {
774
- error: 'Quiz messages must use POST /api/quiz, not /api/visual. The /api/quiz endpoint sends the quiz AND waits for the browser answer in one request.',
775
- });
776
- return;
777
- }
778
-
779
- // Reject game messages — they must use POST /api/game to capture results
780
- if (envelope.type === 'canvas:game') {
781
- sendJson(res, 400, {
782
- error: 'Game messages must use POST /api/game, not /api/visual. The /api/game endpoint sends the game AND waits for the browser result in one request.',
783
- });
784
- return;
785
- }
786
-
787
- // Check if we should render a template
788
- if (envelope.payload && envelope.payload.template) {
789
- const rendered = renderTemplate(envelope.payload.template, envelope.payload.data || {});
790
- if (rendered) {
791
- envelope.payload.renderedHtml = rendered;
792
- }
793
- }
794
-
795
- router.routeVisualCommand(envelope);
796
- sendJson(res, 200, { ok: true, tier: router.tierManager.getTier() });
797
- });
798
- }
799
-
800
- /**
801
- * POST /api/quiz?timeout=30 — send a visual quiz AND wait for the browser answer.
802
- * Combines /api/visual + /api/events/wait into one atomic request so the caller
803
- * cannot accidentally skip the poll step.
1670
+ * POST /api/canvasunified endpoint for ALL visual commands.
1671
+ *
1672
+ * Accepts any canvas:* envelope. If the envelope contains an `await` field,
1673
+ * the response is held until a matching event arrives or timeout expires.
1674
+ *
1675
+ * Without `await`: fire-and-forget (diagrams, dashboard, celebrations, HTML).
1676
+ * With `await`: send visual + long-poll for matching event (quizzes, games, etc).
1677
+ *
1678
+ * Envelope format:
1679
+ * { v, type, payload, source, timestamp, await?: { event, timeout } }
804
1680
  */
805
- function handleApiQuiz(req, res, router, url) {
806
- const timeoutParam = parseInt(url.searchParams.get('timeout') || '30', 10);
807
- const timeoutSec = isNaN(timeoutParam) ? 30 : timeoutParam;
808
- const timeoutMs = Math.min(
809
- Math.max(timeoutSec * 1000, 1000),
810
- MAX_LONG_POLL_TIMEOUT_MS
811
- );
812
-
1681
+ function handleApiCanvas(req, res, router, url) {
813
1682
  readBody(req, (err, body) => {
814
1683
  if (err) {
815
1684
  sendJson(res, 400, { error: 'Invalid request body' });
@@ -822,7 +1691,6 @@ function handleApiQuiz(req, res, router, url) {
822
1691
  return;
823
1692
  }
824
1693
 
825
- // Render template if needed (same as /api/visual)
826
1694
  if (envelope.payload && envelope.payload.template) {
827
1695
  const rendered = renderTemplate(envelope.payload.template, envelope.payload.data || {});
828
1696
  if (rendered) {
@@ -830,67 +1698,20 @@ function handleApiQuiz(req, res, router, url) {
830
1698
  }
831
1699
  }
832
1700
 
833
- // Step 1: Send the quiz to connected clients
834
1701
  router.routeVisualCommand(envelope);
835
1702
 
836
- // Step 2: Wait for the browser answer
837
- // Use res.on('close') req 'close' fires when body is consumed (too early!)
838
- let settled = false;
839
- res.on('close', () => { settled = true; });
840
-
841
- router.pollEventsAsync(timeoutMs).then((events) => {
842
- if (settled) return;
843
- settled = true;
844
- sendJson(res, 200, {
845
- ok: true,
846
- tier: router.tierManager.getTier(),
847
- events,
848
- count: events.length,
849
- });
850
- });
851
- });
852
- }
853
-
854
- /**
855
- * POST /api/game?timeout=120 — send a game to canvas AND wait for the game result.
856
- * Mirrors /api/quiz: combines /api/visual + /api/events/wait into one atomic request
857
- * so hooks cannot steal the event:game-result between the two calls.
858
- */
859
- function handleApiGame(req, res, router, url) {
860
- const timeoutParam = parseInt(url.searchParams.get('timeout') || '120', 10);
861
- const timeoutSec = isNaN(timeoutParam) ? 120 : timeoutParam;
862
- const timeoutMs = Math.min(
863
- Math.max(timeoutSec * 1000, 1000),
864
- MAX_LONG_POLL_TIMEOUT_MS
865
- );
866
-
867
- readBody(req, (err, body) => {
868
- if (err) {
869
- sendJson(res, 400, { error: 'Invalid request body' });
870
- return;
871
- }
872
-
873
- const { valid, envelope, error } = parseEnvelope(body);
874
- if (!valid) {
875
- sendJson(res, 400, { error });
1703
+ const awaitSpec = envelope.await;
1704
+ if (!awaitSpec || !awaitSpec.event) {
1705
+ sendJson(res, 200, { ok: true, tier: router.tierManager.getTier() });
876
1706
  return;
877
1707
  }
878
1708
 
879
- // Render template if needed (same as /api/visual)
880
- if (envelope.payload && envelope.payload.template) {
881
- const rendered = renderTemplate(envelope.payload.template, envelope.payload.data || {});
882
- if (rendered) {
883
- envelope.payload.renderedHtml = rendered;
884
- }
885
- }
886
-
887
- // Step 1: Send the game to connected clients
888
- router.routeVisualCommand(envelope);
1709
+ const timeoutSec = typeof awaitSpec.timeout === 'number' ? awaitSpec.timeout : 30;
1710
+ const timeoutMs = Math.min(
1711
+ Math.max(timeoutSec * 1000, 1000),
1712
+ MAX_LONG_POLL_TIMEOUT_MS
1713
+ );
889
1714
 
890
- // Step 2: Wait for the game result
891
- // IMPORTANT: Use res.on('close'), NOT req.on('close').
892
- // req 'close' fires when the request body is consumed (immediately after parsing),
893
- // but res 'close' fires when the underlying connection is actually closed.
894
1715
  let settled = false;
895
1716
  res.on('close', () => { settled = true; });
896
1717
 
@@ -1031,6 +1852,126 @@ function handleApiTerminalExec(req, res, router) {
1031
1852
  });
1032
1853
  }
1033
1854
 
1855
+ /**
1856
+ * POST /api/pty/create — create a new PTY terminal session.
1857
+ */
1858
+ function handleApiPtyCreate(req, res) {
1859
+ readBody(req, async (err, body) => {
1860
+ if (err) {
1861
+ sendJson(res, 400, { error: 'Invalid request body' });
1862
+ return;
1863
+ }
1864
+ try {
1865
+ const { sessionId, cwd } = await createPtySession({
1866
+ cwd: body?.cwd,
1867
+ cols: body?.cols,
1868
+ rows: body?.rows,
1869
+ shell: body?.shell,
1870
+ env: body?.env,
1871
+ });
1872
+ sendJson(res, 200, { sessionId, cwd });
1873
+ } catch (e) {
1874
+ sendJson(res, 500, { error: e.message });
1875
+ }
1876
+ });
1877
+ }
1878
+
1879
+ /**
1880
+ * POST /api/playground/exec — execute a code snippet and return output.
1881
+ * Supports JavaScript (Node), Python, and Bash.
1882
+ */
1883
+ function handleApiPlaygroundExec(req, res) {
1884
+ readBody(req, (err, body) => {
1885
+ if (err) {
1886
+ sendJson(res, 400, { error: 'Invalid request body' });
1887
+ return;
1888
+ }
1889
+
1890
+ const { code, language } = body || {};
1891
+ if (typeof code !== 'string') {
1892
+ sendJson(res, 400, { error: 'Missing "code" string in request body' });
1893
+ return;
1894
+ }
1895
+
1896
+ const lang = (language || 'javascript').toLowerCase();
1897
+ let cmd;
1898
+ switch (lang) {
1899
+ case 'javascript':
1900
+ case 'js':
1901
+ cmd = `node -e ${JSON.stringify(code)}`;
1902
+ break;
1903
+ case 'python':
1904
+ case 'py':
1905
+ cmd = `python3 -c ${JSON.stringify(code)}`;
1906
+ break;
1907
+ case 'bash':
1908
+ case 'sh':
1909
+ case 'shell':
1910
+ cmd = `sh -c ${JSON.stringify(code)}`;
1911
+ break;
1912
+ case 'typescript':
1913
+ case 'ts':
1914
+ cmd = `node -e ${JSON.stringify(code)}`;
1915
+ break;
1916
+ default:
1917
+ sendJson(res, 400, { error: `Unsupported language: ${lang}` });
1918
+ return;
1919
+ }
1920
+
1921
+ exec(cmd, {
1922
+ timeout: PLAYGROUND_EXEC_TIMEOUT_MS,
1923
+ maxBuffer: PLAYGROUND_MAX_OUTPUT_BYTES,
1924
+ env: { ...process.env, NO_COLOR: '1' },
1925
+ }, (error, stdout, stderr) => {
1926
+ let output = '';
1927
+ if (stdout) output += stdout;
1928
+ if (stderr) output += (output ? '\n' : '') + stderr;
1929
+ if (error && !stdout && !stderr) {
1930
+ output = error.killed
1931
+ ? `Execution timed out after ${PLAYGROUND_EXEC_TIMEOUT_MS / 1000}s`
1932
+ : error.message;
1933
+ }
1934
+ const exitCode = error ? (error.code ?? 1) : 0;
1935
+ sendJson(res, 200, { output, exitCode, language: lang });
1936
+ });
1937
+ });
1938
+ }
1939
+
1940
+ /**
1941
+ * POST /api/workshop/setup — set up a workshop scenario.
1942
+ * Creates a PTY session, writes files, and optionally runs setup commands.
1943
+ */
1944
+ function handleApiWorkshopSetup(req, res, router) {
1945
+ readBody(req, async (err, body) => {
1946
+ if (err) {
1947
+ sendJson(res, 400, { error: 'Invalid request body' });
1948
+ return;
1949
+ }
1950
+
1951
+ try {
1952
+ const { cwd, files, setup, cols, rows } = body || {};
1953
+
1954
+ const { sessionId, cwd: sessionCwd } = await createPtySession({
1955
+ cwd,
1956
+ cols: cols || 120,
1957
+ rows: rows || 30,
1958
+ });
1959
+
1960
+ if (files && Array.isArray(files)) {
1961
+ await writeSessionFiles(sessionId, files);
1962
+ }
1963
+
1964
+ if (setup && Array.isArray(setup)) {
1965
+ runSetupCommands(sessionId, setup);
1966
+ }
1967
+
1968
+ sendJson(res, 200, { sessionId, cwd: sessionCwd });
1969
+ } catch (e) {
1970
+ sendJson(res, 500, { error: e.message });
1971
+ }
1972
+ });
1973
+ }
1974
+
1034
1975
  // --- Helpers ---
1035
1976
 
1036
1977
  function readBody(req, callback) {
@@ -1063,6 +2004,199 @@ function ensureDir(dirPath) {
1063
2004
  }
1064
2005
  }
1065
2006
 
2007
+ /**
2008
+ * Enrich progress modules with metadata (title, description, difficulty)
2009
+ * from the module catalog. Modules in progress get their metadata merged in;
2010
+ * catalog modules not yet started are included as status: 'available'.
2011
+ * @param {object} progressModules - { [slug]: { status, xp_earned, ... } }
2012
+ * @param {Map} moduleMap - from buildModuleMap()
2013
+ * @returns {object} enriched modules object keyed by slug
2014
+ */
2015
+ export function enrichModulesWithMetadata(progressModules, moduleMap) {
2016
+ const enriched = {};
2017
+
2018
+ for (const [slug, mod] of Object.entries(progressModules || {})) {
2019
+ const meta = moduleMap.get(slug);
2020
+ enriched[slug] = {
2021
+ ...mod,
2022
+ title: meta?.title || slug,
2023
+ description: meta?.description || '',
2024
+ difficulty: meta?.difficulty || 'beginner',
2025
+ };
2026
+ }
2027
+
2028
+ for (const [slug, meta] of moduleMap) {
2029
+ if (!enriched[slug]) {
2030
+ enriched[slug] = {
2031
+ status: 'available',
2032
+ title: meta.title || slug,
2033
+ description: meta.description || '',
2034
+ difficulty: meta.difficulty || 'beginner',
2035
+ };
2036
+ }
2037
+ }
2038
+
2039
+ return enriched;
2040
+ }
2041
+
2042
+ /**
2043
+ * Parse a .socrates-progress.yaml file without js-yaml.
2044
+ * Handles the 2-level nested structure: top-level sections (user, modules, journey)
2045
+ * with flat key-value children, plus modules having per-slug sub-objects.
2046
+ */
2047
+ export function parseProgressYaml(raw) {
2048
+ const result = { user: { xp: 0, belt: 'white', modules_completed: 0 }, modules: {}, journey: { active: null, progress: {} } };
2049
+ if (!raw) return result;
2050
+
2051
+ const lines = raw.split('\n');
2052
+ let section = null; // 'user', 'modules', 'journey'
2053
+ let subKey = null; // module slug or journey progress key
2054
+
2055
+ for (const line of lines) {
2056
+ if (line.startsWith('#') || line.trim() === '') continue;
2057
+
2058
+ // Top-level section (no indent)
2059
+ const sectionMatch = line.match(/^(\w[\w_-]*):\s*(.*)$/);
2060
+ if (sectionMatch && !line.startsWith(' ') && !line.startsWith('\t')) {
2061
+ section = sectionMatch[1];
2062
+ subKey = null;
2063
+ const val = sectionMatch[2].trim();
2064
+ if (val && section !== 'user' && section !== 'modules' && section !== 'journey' && section !== 'achievements') {
2065
+ result[section] = parseYamlValue(val);
2066
+ }
2067
+ continue;
2068
+ }
2069
+
2070
+ // 2-space or 4-space indented content
2071
+ const indent2 = line.match(/^ (\w[\w_-]*):\s*(.*)$/);
2072
+ if (indent2) {
2073
+ const key = indent2[1];
2074
+ const val = indent2[2].trim();
2075
+
2076
+ if (section === 'user') {
2077
+ result.user[key] = parseYamlValue(val);
2078
+ subKey = null;
2079
+ } else if (section === 'modules') {
2080
+ if (!val) {
2081
+ subKey = key;
2082
+ result.modules[key] = result.modules[key] || {};
2083
+ } else {
2084
+ subKey = key;
2085
+ result.modules[key] = result.modules[key] || {};
2086
+ }
2087
+ } else if (section === 'journey') {
2088
+ if (key === 'active') {
2089
+ result.journey.active = val === 'null' || val === '' ? null : parseYamlValue(val);
2090
+ } else if (key === 'progress' && !val) {
2091
+ subKey = 'progress';
2092
+ }
2093
+ }
2094
+ continue;
2095
+ }
2096
+
2097
+ // 4-space indented (module fields or journey progress fields)
2098
+ const indent4 = line.match(/^ (\w[\w_-]*):\s*(.*)$/);
2099
+ if (indent4 && subKey) {
2100
+ const key = indent4[1];
2101
+ const val = indent4[2].trim();
2102
+ if (section === 'modules' && result.modules[subKey]) {
2103
+ result.modules[subKey][key] = parseYamlValue(val);
2104
+ } else if (section === 'journey' && subKey === 'progress') {
2105
+ result.journey.progress[key] = parseYamlValue(val);
2106
+ }
2107
+ }
2108
+ }
2109
+
2110
+ return result;
2111
+ }
2112
+
2113
+ function parseYamlValue(val) {
2114
+ if (val === 'null' || val === '~' || val === '') return null;
2115
+ if (val === 'true') return true;
2116
+ if (val === 'false') return false;
2117
+ if (/^-?\d+$/.test(val)) return parseInt(val, 10);
2118
+ if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
2119
+ if (val.startsWith("'") && val.endsWith("'")) return val.slice(1, -1);
2120
+ if (val.startsWith('"') && val.endsWith('"')) return val.slice(1, -1);
2121
+ return val;
2122
+ }
2123
+
2124
+ /**
2125
+ * Serialize a progress object back to YAML (simple 2-level nesting).
2126
+ */
2127
+ export function serializeProgressYaml(progress) {
2128
+ const lines = [];
2129
+
2130
+ if (progress.user) {
2131
+ lines.push('user:');
2132
+ for (const [k, v] of Object.entries(progress.user)) {
2133
+ lines.push(` ${k}: ${formatYamlValue(v)}`);
2134
+ }
2135
+ }
2136
+
2137
+ if (progress.modules && Object.keys(progress.modules).length > 0) {
2138
+ lines.push('modules:');
2139
+ for (const [slug, mod] of Object.entries(progress.modules)) {
2140
+ lines.push(` ${slug}:`);
2141
+ for (const [k, v] of Object.entries(mod)) {
2142
+ lines.push(` ${k}: ${formatYamlValue(v)}`);
2143
+ }
2144
+ }
2145
+ } else {
2146
+ lines.push('modules: {}');
2147
+ }
2148
+
2149
+ if (progress.journey) {
2150
+ lines.push('journey:');
2151
+ lines.push(` active: ${formatYamlValue(progress.journey.active)}`);
2152
+ if (progress.journey.progress && Object.keys(progress.journey.progress).length > 0) {
2153
+ lines.push(' progress:');
2154
+ for (const [k, v] of Object.entries(progress.journey.progress)) {
2155
+ lines.push(` ${k}: ${formatYamlValue(v)}`);
2156
+ }
2157
+ } else {
2158
+ lines.push(' progress: {}');
2159
+ }
2160
+ }
2161
+
2162
+ return lines.join('\n') + '\n';
2163
+ }
2164
+
2165
+ function formatYamlValue(v) {
2166
+ if (v === null || v === undefined) return 'null';
2167
+ if (typeof v === 'boolean') return v ? 'true' : 'false';
2168
+ if (typeof v === 'number') return String(v);
2169
+ if (typeof v === 'string' && /[:#{}[\],&*?|>!%@`]/.test(v)) return `"${v}"`;
2170
+ return String(v);
2171
+ }
2172
+
2173
+ /**
2174
+ * Create a progressProvider that reads/writes .socrates-progress.yaml directly.
2175
+ * Used in standalone bridge mode when no CLI-injected provider is available.
2176
+ */
2177
+ export function createFileProgressProvider(filePath) {
2178
+ return {
2179
+ getProgress() {
2180
+ try {
2181
+ if (!existsSync(filePath)) {
2182
+ return { user: { xp: 0, belt: 'white', modules_completed: 0 }, modules: {}, journey: { active: null, progress: {} } };
2183
+ }
2184
+ const raw = readFileSync(filePath, 'utf-8');
2185
+ return parseProgressYaml(raw);
2186
+ } catch {
2187
+ return { user: { xp: 0, belt: 'white', modules_completed: 0 }, modules: {}, journey: { active: null, progress: {} } };
2188
+ }
2189
+ },
2190
+ saveProgress(progress) {
2191
+ try {
2192
+ writeFileSync(filePath, serializeProgressYaml(progress), 'utf-8');
2193
+ } catch (err) {
2194
+ console.error('[bridge] Failed to save progress:', err.message);
2195
+ }
2196
+ },
2197
+ };
2198
+ }
2199
+
1066
2200
  // --- Standalone execution ---
1067
2201
  if (process.argv[1] && process.argv[1].includes('bridge/src/server.js')) {
1068
2202
  // Prevent unhandled errors from crashing the server
@@ -1075,5 +2209,7 @@ if (process.argv[1] && process.argv[1].includes('bridge/src/server.js')) {
1075
2209
 
1076
2210
  const port = parseInt(process.env.PORT || process.argv[2] || DEFAULT_PORT, 10);
1077
2211
  const pluginDir = resolve(__dirname, '..', '..', 'plugin');
1078
- startServer({ port, pluginDir });
2212
+ const progressFile = resolve(process.cwd(), '.socrates-progress.yaml');
2213
+ const progressProvider = createFileProgressProvider(progressFile);
2214
+ startServer({ port, pluginDir, progressProvider });
1079
2215
  }