@shaykec/bridge 0.4.19 → 0.4.21
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.
- package/README.md +2 -2
- package/canvas-dist/assets/{_basePickBy-BOTBlJNd.js → _basePickBy-BovdgFIW.js} +1 -1
- package/canvas-dist/assets/_basePickBy-BtkHe2u_.js +1 -0
- package/canvas-dist/assets/_basePickBy-C0936578.js +1 -0
- package/canvas-dist/assets/_basePickBy-CE2Qvuh7.js +1 -0
- package/canvas-dist/assets/_basePickBy-DV6sX4CG.js +1 -0
- package/canvas-dist/assets/_basePickBy-DZX6ZNMT.js +1 -0
- package/canvas-dist/assets/{_baseUniq-EF6Y2_Wm.js → _baseUniq-B7dN28TM.js} +1 -1
- package/canvas-dist/assets/_baseUniq-Cl23fCdR.js +1 -0
- package/canvas-dist/assets/_baseUniq-CojWFw7B.js +1 -0
- package/canvas-dist/assets/_baseUniq-DA640BJl.js +1 -0
- package/canvas-dist/assets/_baseUniq-Ds-62CCj.js +1 -0
- package/canvas-dist/assets/_baseUniq-KG7SRw9H.js +1 -0
- package/canvas-dist/assets/{arc-C_vIirh2.js → arc-7E9FFKlC.js} +1 -1
- package/canvas-dist/assets/arc-BSMfRZtt.js +1 -0
- package/canvas-dist/assets/arc-C6nT-koR.js +1 -0
- package/canvas-dist/assets/arc-D_fOnjmo.js +1 -0
- package/canvas-dist/assets/arc-Khfvgkr3.js +1 -0
- package/canvas-dist/assets/arc-ieS-i42x.js +1 -0
- package/canvas-dist/assets/{architectureDiagram-VXUJARFQ-EvM6tQ7I.js → architectureDiagram-VXUJARFQ-DF4t6GQD.js} +1 -1
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DXgSlsio.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DiomxPB4.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-DnFaxvXD.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-Dt38C0LJ.js +36 -0
- package/canvas-dist/assets/architectureDiagram-VXUJARFQ-egbtMwua.js +36 -0
- package/canvas-dist/assets/{blockDiagram-VD42YOAC-B_rbZyqc.js → blockDiagram-VD42YOAC-CUNKQd-b.js} +1 -1
- package/canvas-dist/assets/blockDiagram-VD42YOAC-D-NiLXxd.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-Dx6Dh9gg.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-_r-PmlQy.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-bvYKZLMc.js +122 -0
- package/canvas-dist/assets/blockDiagram-VD42YOAC-l85QT9Ig.js +122 -0
- package/canvas-dist/assets/{c4Diagram-YG6GDRKO-J9PHecY3.js → c4Diagram-YG6GDRKO-BWKCTyQi.js} +1 -1
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-CbXs2xzC.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-CjiS-GNK.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-D7SnLlHp.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-RTTCSVf2.js +10 -0
- package/canvas-dist/assets/c4Diagram-YG6GDRKO-yvqJ_AqX.js +10 -0
- package/canvas-dist/assets/channel-CSXq7GP6.js +1 -0
- package/canvas-dist/assets/channel-CvujjGiJ.js +1 -0
- package/canvas-dist/assets/channel-D959Iony.js +1 -0
- package/canvas-dist/assets/channel-DOSwCnrB.js +1 -0
- package/canvas-dist/assets/channel-sw61LzxF.js +1 -0
- package/canvas-dist/assets/channel-vZVnNhOK.js +1 -0
- package/canvas-dist/assets/{chunk-4BX2VUAB-DjcN96Mk.js → chunk-4BX2VUAB-BBjuAwXr.js} +1 -1
- package/canvas-dist/assets/chunk-4BX2VUAB-BXRNyucU.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-Bgq5Z77T.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-BuoMCMCr.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-COD5n7vg.js +1 -0
- package/canvas-dist/assets/chunk-4BX2VUAB-K8DepKJO.js +1 -0
- package/canvas-dist/assets/{chunk-55IACEB6-CTdcUQSV.js → chunk-55IACEB6-Bic_bMrQ.js} +1 -1
- package/canvas-dist/assets/chunk-55IACEB6-DEy2QUDq.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-Dcgbmfzg.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-DfmuNm_E.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-DlQRcczm.js +1 -0
- package/canvas-dist/assets/chunk-55IACEB6-p2qMY-fm.js +1 -0
- package/canvas-dist/assets/{chunk-B4BG7PRW-Dcov7eRi.js → chunk-B4BG7PRW-BpbyxBP2.js} +1 -1
- package/canvas-dist/assets/chunk-B4BG7PRW-CCPqvPrP.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-CEeDPAki.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-D2UFN_2M.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-DFI5h6HC.js +165 -0
- package/canvas-dist/assets/chunk-B4BG7PRW-DKOiFGMU.js +165 -0
- package/canvas-dist/assets/{chunk-DI55MBZ5-DUJCBZzM.js → chunk-DI55MBZ5-BV6nHjNQ.js} +1 -1
- package/canvas-dist/assets/chunk-DI55MBZ5-CEZJmC0E.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DOZT99Ek.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DmC2LoG2.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-DpkcJdZP.js +220 -0
- package/canvas-dist/assets/chunk-DI55MBZ5-fVTGx0zh.js +220 -0
- package/canvas-dist/assets/{chunk-FMBD7UC4-EfGA9ufe.js → chunk-FMBD7UC4-BOCyQpI7.js} +1 -1
- package/canvas-dist/assets/chunk-FMBD7UC4-C76FrRL8.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-CAq-btWc.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-CidVsej6.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-DPpfskdX.js +15 -0
- package/canvas-dist/assets/chunk-FMBD7UC4-DnLtclge.js +15 -0
- package/canvas-dist/assets/{chunk-QN33PNHL-Cu6V1xBU.js → chunk-QN33PNHL-BclpCUi8.js} +1 -1
- package/canvas-dist/assets/chunk-QN33PNHL-DDUw8IU1.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-DdJFAUXw.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-DjV4jUn9.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-N-HTycqU.js +1 -0
- package/canvas-dist/assets/chunk-QN33PNHL-sd8p21DW.js +1 -0
- package/canvas-dist/assets/{chunk-QZHKN3VN-avF3sH_r.js → chunk-QZHKN3VN-B6mT-JkP.js} +1 -1
- package/canvas-dist/assets/chunk-QZHKN3VN-BCo8pc7x.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-C8IIu6es.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-D9FF492U.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-DWMbUjXT.js +1 -0
- package/canvas-dist/assets/chunk-QZHKN3VN-l5FBJ77g.js +1 -0
- package/canvas-dist/assets/{chunk-TZMSLE5B-CkWW-qpk.js → chunk-TZMSLE5B-BASt-UWt.js} +1 -1
- package/canvas-dist/assets/chunk-TZMSLE5B-BCfaZWLT.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-BKIk_hBR.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-C4pt-Ir8.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-DwGlELvo.js +1 -0
- package/canvas-dist/assets/chunk-TZMSLE5B-jJKG-WvJ.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-B7YQfPU4.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-BZ61MaHY.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CGseYor2.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CKzOc99J.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-Ce_LPjwW.js +1 -0
- package/canvas-dist/assets/classDiagram-2ON5EDUG-DorPdibv.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-B7YQfPU4.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-BZ61MaHY.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CGseYor2.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CKzOc99J.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-Ce_LPjwW.js +1 -0
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-DorPdibv.js +1 -0
- package/canvas-dist/assets/clone-74KSto7H.js +1 -0
- package/canvas-dist/assets/clone-CJQgAYVe.js +1 -0
- package/canvas-dist/assets/clone-DLeTuhHE.js +1 -0
- package/canvas-dist/assets/clone-D_IHK_lQ.js +1 -0
- package/canvas-dist/assets/clone-DxMUv1L9.js +1 -0
- package/canvas-dist/assets/clone-UNKf_nED.js +1 -0
- package/canvas-dist/assets/{cose-bilkent-S5V4N54A-DDE4zf7X.js → cose-bilkent-S5V4N54A-BTyQiCkr.js} +1 -1
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-BtPAe24N.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-DIjE7V3m.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-DKL_BGvE.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-LZ4OsCLU.js +1 -0
- package/canvas-dist/assets/cose-bilkent-S5V4N54A-XWeJtgga.js +1 -0
- package/canvas-dist/assets/{dagre-6UL2VRFP-BD6MGb7B.js → dagre-6UL2VRFP-BJ2vcFwR.js} +1 -1
- package/canvas-dist/assets/dagre-6UL2VRFP-C1FlE5s8.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-C3BWFgl6.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-CUnx73Rf.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-Do10BY1y.js +4 -0
- package/canvas-dist/assets/dagre-6UL2VRFP-rOZEkrsg.js +4 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-BGi_qzbq.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-C3Nv7h_j.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-CsMy-r0n.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-Dj8g7kGt.js +24 -0
- package/canvas-dist/assets/diagram-PSM6KHXK-Dxb1w_7r.js +24 -0
- package/canvas-dist/assets/{diagram-PSM6KHXK-yyu-ytzf.js → diagram-PSM6KHXK-kVMBkEyV.js} +1 -1
- package/canvas-dist/assets/{diagram-QEK2KX5R-B_H957Uf.js → diagram-QEK2KX5R-4bsrr1WZ.js} +1 -1
- package/canvas-dist/assets/diagram-QEK2KX5R-Bv7BmKfI.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-C_FLN6hv.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-Csuk5L3z.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-D5Aszgz4.js +43 -0
- package/canvas-dist/assets/diagram-QEK2KX5R-DX58f87l.js +43 -0
- package/canvas-dist/assets/{diagram-S2PKOQOG-DuebuBVv.js → diagram-S2PKOQOG-1Q7hwiSd.js} +1 -1
- package/canvas-dist/assets/diagram-S2PKOQOG-Bz9Vxi5V.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-CdWgZIIc.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-DBicbKFU.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-DsXKwPtU.js +24 -0
- package/canvas-dist/assets/diagram-S2PKOQOG-L_SMHLXs.js +24 -0
- package/canvas-dist/assets/{erDiagram-Q2GNP2WA-AxqPt6IZ.js → erDiagram-Q2GNP2WA-BYu7fh6H.js} +1 -1
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-CvnQ69BF.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-D3xm-Tdm.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-DIPpD8sj.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-DNgu6dMd.js +60 -0
- package/canvas-dist/assets/erDiagram-Q2GNP2WA-Decm8aB4.js +60 -0
- package/canvas-dist/assets/{flowDiagram-NV44I4VS-mDhW3D3Q.js → flowDiagram-NV44I4VS-2ymk2kw2.js} +1 -1
- package/canvas-dist/assets/flowDiagram-NV44I4VS-BEPFOt6U.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-BwqXYGfK.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-CS1jax_z.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-DQz5bf7r.js +162 -0
- package/canvas-dist/assets/flowDiagram-NV44I4VS-KW4T1sqF.js +162 -0
- package/canvas-dist/assets/{ganttDiagram-JELNMOA3-sA8pHJPp.js → ganttDiagram-JELNMOA3-B811prZt.js} +1 -1
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-C75pWm7X.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-CWsbo0fn.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-CbJozPBN.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-Co0cFt4c.js +267 -0
- package/canvas-dist/assets/ganttDiagram-JELNMOA3-I4PDqrRh.js +267 -0
- package/canvas-dist/assets/{gitGraphDiagram-V2S2FVAM-CvLzvhKr.js → gitGraphDiagram-V2S2FVAM-B-z0cLPt.js} +1 -1
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-Be40z-LF.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BejNaAVm.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-BqWDYr0X.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-DSvWGY-e.js +65 -0
- package/canvas-dist/assets/gitGraphDiagram-V2S2FVAM-HLYbyNJ5.js +65 -0
- package/canvas-dist/assets/{graph-BVZqMrwW.js → graph-BE8KKsdf.js} +1 -1
- package/canvas-dist/assets/graph-D6DzzszU.js +1 -0
- package/canvas-dist/assets/graph-DFR8Y_8s.js +1 -0
- package/canvas-dist/assets/graph-Da385cDY.js +1 -0
- package/canvas-dist/assets/graph-MU7gZz2B.js +1 -0
- package/canvas-dist/assets/graph-wjSBJwnf.js +1 -0
- package/canvas-dist/assets/index--ztw-8Rw.js +647 -0
- package/canvas-dist/assets/{index-CF3qc2Xb.js → index-5TpIM6B1.js} +1 -1
- package/canvas-dist/assets/index-6GBZ9nXN.css +32 -0
- package/canvas-dist/assets/index-BSswTuBk.js +11 -0
- package/canvas-dist/assets/index-BVvhMmjs.js +11 -0
- package/canvas-dist/assets/index-BY92Mj5g.js +572 -0
- package/canvas-dist/assets/index-CV7palC3.js +572 -0
- package/canvas-dist/assets/index-D9bmQGsB.js +11 -0
- package/canvas-dist/assets/index-DDIKkGv8.js +592 -0
- package/canvas-dist/assets/index-Dyo0NkPb.js +574 -0
- package/canvas-dist/assets/index-iQWajCow.js +572 -0
- package/canvas-dist/assets/index-m68YlAMU.js +11 -0
- package/canvas-dist/assets/index-mEoP57az.js +11 -0
- package/canvas-dist/assets/{infoDiagram-HS3SLOUP-D1Kg3Q9d.js → infoDiagram-HS3SLOUP--9BirqgJ.js} +1 -1
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-CSJVED2y.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-D68HIb2t.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-DK2VLGGz.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-PaFhn4yD.js +2 -0
- package/canvas-dist/assets/infoDiagram-HS3SLOUP-zLNG47sU.js +2 -0
- package/canvas-dist/assets/{journeyDiagram-XKPGCS4Q-D7ogbx9z.js → journeyDiagram-XKPGCS4Q-Bue2dR2X.js} +1 -1
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-CrgZfpdU.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-DUxWmkkC.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-OTFkv4pd.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-eK2_Zuu3.js +139 -0
- package/canvas-dist/assets/journeyDiagram-XKPGCS4Q-uds5Tz8D.js +139 -0
- package/canvas-dist/assets/{kanban-definition-3W4ZIXB7-CDcnICM9.js → kanban-definition-3W4ZIXB7-BETdiI7I.js} +1 -1
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-BdVh7KdN.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-Cxl8UM9S.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-DVPlx3I2.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-LtNWeoYB.js +89 -0
- package/canvas-dist/assets/kanban-definition-3W4ZIXB7-uvhEMvyE.js +89 -0
- package/canvas-dist/assets/{layout-CuaK7i3M.js → layout-1OzszN14.js} +1 -1
- package/canvas-dist/assets/layout-CJSupFcF.js +1 -0
- package/canvas-dist/assets/layout-DFRmxN_c.js +1 -0
- package/canvas-dist/assets/layout-DSu-zk7y.js +1 -0
- package/canvas-dist/assets/layout-TGcrvApd.js +1 -0
- package/canvas-dist/assets/layout-eStc8SYK.js +1 -0
- package/canvas-dist/assets/{linear-CLSTOJ0g.js → linear-9qlE6xa7.js} +1 -1
- package/canvas-dist/assets/linear-CBfFWnLD.js +1 -0
- package/canvas-dist/assets/linear-Cv4ai8Hq.js +1 -0
- package/canvas-dist/assets/linear-DDzz65E6.js +1 -0
- package/canvas-dist/assets/linear-wbIqhwDf.js +1 -0
- package/canvas-dist/assets/linear-wyNKl76F.js +1 -0
- package/canvas-dist/assets/{mindmap-definition-VGOIOE7T-TrK7CIKt.js → mindmap-definition-VGOIOE7T-3l4YzhEM.js} +1 -1
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-B-KkpNlw.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-DHMHWgmT.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-Dqfyg4Z2.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-NeRYOzsq.js +68 -0
- package/canvas-dist/assets/mindmap-definition-VGOIOE7T-xyu628P9.js +68 -0
- package/canvas-dist/assets/{pieDiagram-ADFJNKIX-BcIKTRbi.js → pieDiagram-ADFJNKIX-BWNzVAGj.js} +1 -1
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-Bm3PXYs-.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-BvvN7VvQ.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-BwU7AN7W.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-CHgwWCaM.js +30 -0
- package/canvas-dist/assets/pieDiagram-ADFJNKIX-DlZc8YOh.js +30 -0
- package/canvas-dist/assets/{quadrantDiagram-AYHSOK5B-EOHXFGoQ.js → quadrantDiagram-AYHSOK5B-B-Zd8OFp.js} +1 -1
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-B1CnJyxI.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C0Qo00b9.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-C9bx3nEJ.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-UHENkiRO.js +7 -0
- package/canvas-dist/assets/quadrantDiagram-AYHSOK5B-jKfurTPU.js +7 -0
- package/canvas-dist/assets/{requirementDiagram-UZGBJVZJ-CJ8lImGs.js → requirementDiagram-UZGBJVZJ-BPpNNusD.js} +1 -1
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-BwZF1NIK.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-CaT3Frtk.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-Dfoz7R_7.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-DsrX4TT-.js +64 -0
- package/canvas-dist/assets/requirementDiagram-UZGBJVZJ-dmouSXOl.js +64 -0
- package/canvas-dist/assets/{sankeyDiagram-TZEHDZUN-4cANY87E.js → sankeyDiagram-TZEHDZUN-BEy-A1Fu.js} +1 -1
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BViMBiAQ.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-BqrM-qWN.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DRkRC9qB.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-DbuzKCtn.js +10 -0
- package/canvas-dist/assets/sankeyDiagram-TZEHDZUN-_aHMKbpw.js +10 -0
- package/canvas-dist/assets/{sequenceDiagram-WL72ISMW-D9HrEsci.js → sequenceDiagram-WL72ISMW-B8FOaL2Q.js} +1 -1
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-C02NQwOB.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CgyHivPj.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-CzW1WaEm.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-DJhHI1pe.js +145 -0
- package/canvas-dist/assets/sequenceDiagram-WL72ISMW-VFkpAeoG.js +145 -0
- package/canvas-dist/assets/{stateDiagram-FKZM4ZOC-qVbMjauZ.js → stateDiagram-FKZM4ZOC-BSqFX4PJ.js} +1 -1
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-BnXhhxkN.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-ClARVrvt.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-CuC6xesY.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-DcAiGjph.js +1 -0
- package/canvas-dist/assets/stateDiagram-FKZM4ZOC-aBg0hjTp.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-8fib9ftc.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-B-DO0ZqO.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-BksbsE4k.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-C2DJCNPK.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-CeA5jba6.js +1 -0
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-zsAyq0tK.js +1 -0
- package/canvas-dist/assets/{timeline-definition-IT6M3QCI-DDBlkydm.js → timeline-definition-IT6M3QCI-BaHdYD2h.js} +1 -1
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-Bl2hg8IM.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-CrVwLiGm.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-DrXGRjnB.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-cYAwshf6.js +61 -0
- package/canvas-dist/assets/timeline-definition-IT6M3QCI-flyL0y-3.js +61 -0
- package/canvas-dist/assets/{treemap-GDKQZRPO-D4a8udjO.js → treemap-GDKQZRPO-C4Hg8kJ_.js} +1 -1
- package/canvas-dist/assets/treemap-GDKQZRPO-DVY2G9qY.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-DpLWPA1z.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-Ds86cUVw.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-DwmoI6tH.js +162 -0
- package/canvas-dist/assets/treemap-GDKQZRPO-SsGFkgVd.js +162 -0
- package/canvas-dist/assets/{xychartDiagram-PRI3JC2R-DteXAAAu.js → xychartDiagram-PRI3JC2R-B9c1iLBf.js} +1 -1
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-BpX6MPWa.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CEgW_j0p.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CSEFGEQX.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-CnG4XoMc.js +7 -0
- package/canvas-dist/assets/xychartDiagram-PRI3JC2R-Dftj3Bt3.js +7 -0
- package/canvas-dist/index.html +3 -2
- package/package.json +4 -2
- package/src/protocol.js +1 -1
- package/src/protocol.test.js +1 -1
- package/src/pty-manager.js +281 -0
- package/src/pty-manager.test.js +212 -0
- package/src/router.js +31 -1
- package/src/router.test.js +1 -1
- package/src/sdk-e2e.test.js +101 -0
- package/src/server.e2e.test.js +812 -177
- package/src/server.js +1516 -264
- package/src/session-store.js +260 -0
- package/src/session-store.test.js +235 -0
- package/src/templates.js +1 -1
- package/src/templates.test.js +1 -1
- package/src/terminal.js +3 -3
- package/src/terminal.test.js +12 -12
- package/src/visual-interceptor.js +450 -0
- package/src/visual-interceptor.test.js +943 -0
- package/src/workshop-parser.js +251 -0
- package/src/workshop-parser.test.js +179 -0
- package/templates/celebrate.html +1 -1
- package/templates/code-playground.html +1 -1
- package/templates/dashboard.html +7 -8
- package/templates/diagram-architecture.html +1 -1
- package/templates/diagram-flow.html +1 -1
- package/templates/diagram-mermaid.html +1 -1
- package/templates/game-speed-round.html +1 -1
- package/templates/quiz-drag-order.html +1 -1
- package/templates/quiz-fill-blank.html +1 -1
- package/templates/quiz-matching.html +1 -1
- package/templates/quiz-timed-choice.html +1 -1
- package/templates/welcome.html +7 -7
- package/canvas-dist/assets/channel-saCUO1KA.js +0 -1
- package/canvas-dist/assets/classDiagram-2ON5EDUG-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/classDiagram-v2-WZHVMYZB-CBLbQwHx.js +0 -1
- package/canvas-dist/assets/clone-DXnda9BY.js +0 -1
- package/canvas-dist/assets/index-DYNtb52W.js +0 -426
- package/canvas-dist/assets/stateDiagram-v2-4FDKWEC3-MT16RLO4.js +0 -1
- package/src/claude-session.js +0 -414
- package/src/claude-session.test.js +0 -326
package/src/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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,16 +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,
|
|
41
|
+
ACHIEVEMENTS,
|
|
42
|
+
BELTS,
|
|
43
|
+
PLAYGROUND_EXEC_TIMEOUT_MS,
|
|
44
|
+
PLAYGROUND_MAX_OUTPUT_BYTES,
|
|
40
45
|
} from '@shaykec/shared';
|
|
41
46
|
|
|
42
47
|
import { TierManager, generateClientId, validateHandshake } from './protocol.js';
|
|
43
48
|
import { EventRouter } from './router.js';
|
|
44
49
|
import { renderTemplate, listTemplates } from './templates.js';
|
|
45
50
|
import { createSession, execCommand, getSession, destroySession, cleanupIdleSessions } from './terminal.js';
|
|
46
|
-
import {
|
|
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';
|
|
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';
|
|
47
68
|
|
|
48
69
|
const __filename = fileURLToPath(import.meta.url);
|
|
49
70
|
const __dirname = dirname(__filename);
|
|
@@ -72,22 +93,230 @@ const MIME_TYPES = {
|
|
|
72
93
|
};
|
|
73
94
|
|
|
74
95
|
/**
|
|
75
|
-
* Start the
|
|
96
|
+
* Start the Socrates bridge server.
|
|
76
97
|
* @param {object} [options]
|
|
77
98
|
* @param {number} [options.port=3456] - Port number
|
|
78
99
|
* @param {function} [options.onReady] - Callback when server is listening
|
|
79
100
|
* @param {function} [options.onTierChange] - Callback(oldTier, newTier) on tier changes
|
|
80
|
-
* @param {object} [options.progressProvider] - Object with getProgress()
|
|
101
|
+
* @param {object} [options.progressProvider] - Object with getProgress() and saveProgress() methods
|
|
81
102
|
* @returns {{ server: object, tierManager: TierManager, router: EventRouter, close: function }}
|
|
82
103
|
*/
|
|
83
104
|
export function startServer(options = {}) {
|
|
84
105
|
const port = options.port || DEFAULT_PORT;
|
|
85
106
|
const tierManager = new TierManager();
|
|
86
107
|
const router = new EventRouter(tierManager);
|
|
87
|
-
const chatSession = new ClaudeSessionManager();
|
|
88
108
|
|
|
89
|
-
//
|
|
90
|
-
const
|
|
109
|
+
// Chat is delegated entirely to @shaykec/agent-web
|
|
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
|
+
};
|
|
217
|
+
if (options.pluginDir) {
|
|
218
|
+
agentConfig.plugins = [{ type: 'local', path: options.pluginDir }];
|
|
219
|
+
}
|
|
220
|
+
const autoRoutedMediaUrls = new Set();
|
|
221
|
+
const sessionTitleSet = new Set();
|
|
222
|
+
|
|
223
|
+
const agentServer = createAgentServer({
|
|
224
|
+
basePath: '/api',
|
|
225
|
+
config: agentConfig,
|
|
226
|
+
hooks: {
|
|
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
|
+
|
|
254
|
+
const data = JSON.stringify(envelope);
|
|
255
|
+
tierManager.broadcastWs(data);
|
|
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
|
+
}
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
const chatMiddleware = agentServer.middleware();
|
|
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
|
+
};
|
|
91
320
|
|
|
92
321
|
if (options.onTierChange) {
|
|
93
322
|
tierManager.onTierChange(options.onTierChange);
|
|
@@ -96,47 +325,107 @@ export function startServer(options = {}) {
|
|
|
96
325
|
// Ensure inbox directory exists
|
|
97
326
|
ensureDir(INBOX_DIR);
|
|
98
327
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* @returns {function}
|
|
104
|
-
*/
|
|
105
|
-
function createTargetedSend(clientId) {
|
|
106
|
-
return (envelope) => {
|
|
107
|
-
const data = JSON.stringify(envelope);
|
|
108
|
-
if (!tierManager.sendToClient(clientId, data)) {
|
|
109
|
-
// Client disconnected — broadcast as fallback
|
|
110
|
-
tierManager.broadcastWs(data);
|
|
111
|
-
tierManager.broadcastSse(data);
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
}
|
|
328
|
+
// Create HTTP server
|
|
329
|
+
const server = createServer((req, res) => {
|
|
330
|
+
handleRequest(req, res, tierManager, router, options, chatMiddleware, agentServer);
|
|
331
|
+
});
|
|
115
332
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
333
|
+
// Both WSSes use noServer to avoid ws library destroying sockets
|
|
334
|
+
// on path mismatch (ws docs: use noServer for multiple WSSes)
|
|
335
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
336
|
+
const chatWss = new WebSocketServer({ noServer: true });
|
|
337
|
+
const ptyWss = new WebSocketServer({ noServer: true });
|
|
338
|
+
|
|
339
|
+
server.on('upgrade', (req, socket, head) => {
|
|
340
|
+
const { pathname } = new URL(req.url, `http://${req.headers.host}`);
|
|
341
|
+
if (pathname === '/ws') {
|
|
342
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
343
|
+
wss.emit('connection', ws, req);
|
|
344
|
+
});
|
|
345
|
+
} else if (pathname === '/api/ws') {
|
|
346
|
+
chatWss.handleUpgrade(req, socket, head, (ws) => {
|
|
347
|
+
chatWss.emit('connection', ws, req);
|
|
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);
|
|
123
353
|
});
|
|
354
|
+
} else {
|
|
355
|
+
socket.destroy();
|
|
124
356
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
357
|
+
});
|
|
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;
|
|
130
364
|
}
|
|
131
|
-
};
|
|
132
365
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
|
|
403
|
+
// Wire agent-web transport to the chat WSS
|
|
404
|
+
chatWss.on('connection', (ws) => {
|
|
405
|
+
agentServer.transport.handleWsConnection(
|
|
406
|
+
ws,
|
|
407
|
+
() => {},
|
|
408
|
+
(clientId, envelope) => {
|
|
409
|
+
if (envelope.type === 'chat:send') {
|
|
410
|
+
const sessionId = envelope.sessionId || envelope.payload?.sessionId;
|
|
411
|
+
const text = envelope.payload?.text;
|
|
412
|
+
if (sessionId && text) {
|
|
413
|
+
agentServer.sessions.sendMessage(sessionId, text).catch(err => {
|
|
414
|
+
console.error('[agent-web] sendMessage error:', err.message);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (envelope.type === 'chat:stop') {
|
|
419
|
+
const sessionId = envelope.sessionId || envelope.payload?.sessionId;
|
|
420
|
+
if (sessionId) {
|
|
421
|
+
agentServer.sessions.stopSession(sessionId).catch(() => {});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
);
|
|
136
426
|
});
|
|
137
427
|
|
|
138
|
-
|
|
139
|
-
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
428
|
+
agentServer.transport.startHeartbeat();
|
|
140
429
|
|
|
141
430
|
wss.on('connection', (ws, req) => {
|
|
142
431
|
const clientId = generateClientId();
|
|
@@ -185,7 +474,27 @@ export function startServer(options = {}) {
|
|
|
185
474
|
return;
|
|
186
475
|
}
|
|
187
476
|
|
|
188
|
-
//
|
|
477
|
+
// Intercept chat messages and forward to agent-web session manager
|
|
478
|
+
const { valid: msgValid, envelope: msgEnv } = parseEnvelope(data);
|
|
479
|
+
if (msgValid && msgEnv.type === 'chat:send') {
|
|
480
|
+
const sessionId = msgEnv.sessionId || msgEnv.payload?.sessionId;
|
|
481
|
+
const text = msgEnv.payload?.text;
|
|
482
|
+
if (sessionId && text) {
|
|
483
|
+
agentServer.sessions.sendMessage(sessionId, text).catch(err => {
|
|
484
|
+
console.error('[bridge] chat:send error:', err.message);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (msgValid && msgEnv.type === 'chat:stop') {
|
|
490
|
+
const sessionId = msgEnv.sessionId || msgEnv.payload?.sessionId;
|
|
491
|
+
if (sessionId) {
|
|
492
|
+
agentServer.sessions.stopSession(sessionId).catch(() => {});
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// After handshake, route visual/event messages through the router
|
|
189
498
|
const result = router.handleWsMessage(data, clientId);
|
|
190
499
|
if (!result.handled && result.error) {
|
|
191
500
|
ws.send(JSON.stringify({ error: result.error }));
|
|
@@ -211,11 +520,14 @@ export function startServer(options = {}) {
|
|
|
211
520
|
tierManager.startHeartbeat();
|
|
212
521
|
|
|
213
522
|
// Periodically clean up idle terminal sessions (every 60s)
|
|
214
|
-
const terminalCleanupInterval = setInterval(() =>
|
|
523
|
+
const terminalCleanupInterval = setInterval(() => {
|
|
524
|
+
cleanupIdleSessions();
|
|
525
|
+
cleanupIdlePtySessions();
|
|
526
|
+
}, 60000);
|
|
215
527
|
|
|
216
528
|
// Start listening
|
|
217
529
|
server.listen(port, () => {
|
|
218
|
-
console.log(`
|
|
530
|
+
console.log(`Socrates bridge server running on http://localhost:${port}`);
|
|
219
531
|
console.log(` WebSocket /ws — bidirectional (extension + canvas)`);
|
|
220
532
|
console.log(` SSE /sse — server-push (canvas-only)`);
|
|
221
533
|
console.log(` POST /api/event — receive user events`);
|
|
@@ -224,15 +536,16 @@ export function startServer(options = {}) {
|
|
|
224
536
|
console.log(` GET /api/progress — progress data`);
|
|
225
537
|
console.log(` GET /api/events/wait — long-poll for events`);
|
|
226
538
|
console.log(` POST /api/terminal/exec — execute terminal commands`);
|
|
227
|
-
console.log(` POST /api/
|
|
228
|
-
console.log(` POST /api/
|
|
229
|
-
console.log(`
|
|
230
|
-
console.log(` POST /api/
|
|
231
|
-
console.log(` POST /api/
|
|
232
|
-
console.log(`
|
|
233
|
-
console.log(`
|
|
234
|
-
console.log(`
|
|
235
|
-
console.log(`
|
|
539
|
+
console.log(` POST /api/canvas — unified visual commands (with optional await)`);
|
|
540
|
+
console.log(` POST /api/pty/create — create 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`);
|
|
544
|
+
console.log(` REST /api/chat/* — chat (via @shaykec/agent-web)`);
|
|
545
|
+
console.log(` REST /api/sessions — session management (list/get/update/delete/purge)`);
|
|
546
|
+
console.log(` WebSocket /api/ws — chat WebSocket`);
|
|
547
|
+
console.log(` SSE /api/sse — chat SSE`);
|
|
548
|
+
console.log(` GET /health — health check`);
|
|
236
549
|
if (existsSync(CANVAS_DIST)) {
|
|
237
550
|
console.log(` Static / — canvas app from ${CANVAS_DIST}`);
|
|
238
551
|
}
|
|
@@ -245,24 +558,23 @@ export function startServer(options = {}) {
|
|
|
245
558
|
|
|
246
559
|
const close = () => {
|
|
247
560
|
clearInterval(terminalCleanupInterval);
|
|
248
|
-
|
|
249
|
-
sessionClientMap.clear();
|
|
561
|
+
agentServer.close().catch(() => {});
|
|
250
562
|
router.cancelWaiters();
|
|
251
563
|
tierManager.stopHeartbeat();
|
|
252
564
|
wss.close();
|
|
253
565
|
server.close();
|
|
254
566
|
};
|
|
255
567
|
|
|
256
|
-
return { server, wss, tierManager, router,
|
|
568
|
+
return { server, wss, tierManager, router, agentServer, close };
|
|
257
569
|
}
|
|
258
570
|
|
|
259
571
|
/**
|
|
260
572
|
* Handle HTTP requests.
|
|
261
573
|
*/
|
|
262
|
-
function handleRequest(req, res, tierManager, router, options,
|
|
574
|
+
function handleRequest(req, res, tierManager, router, options, chatMiddleware, agentServer) {
|
|
263
575
|
// CORS headers
|
|
264
576
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
265
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
577
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
266
578
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
267
579
|
|
|
268
580
|
if (req.method === 'OPTIONS') {
|
|
@@ -274,7 +586,21 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
|
|
|
274
586
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
275
587
|
const pathname = url.pathname;
|
|
276
588
|
|
|
277
|
-
// ---
|
|
589
|
+
// --- Chat endpoints delegated to @shaykec/agent-web ---
|
|
590
|
+
if (pathname.startsWith('/api/chat') || pathname === '/api/sse' || pathname === '/api/health') {
|
|
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
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// --- SSE endpoint (visual/events) ---
|
|
278
604
|
if (req.method === 'GET' && pathname === '/sse') {
|
|
279
605
|
handleSse(req, res, tierManager);
|
|
280
606
|
return;
|
|
@@ -301,6 +627,11 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
|
|
|
301
627
|
return;
|
|
302
628
|
}
|
|
303
629
|
|
|
630
|
+
if (req.method === 'GET' && pathname === '/api/constants') {
|
|
631
|
+
sendJson(res, 200, { belts: BELTS });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
304
635
|
if (req.method === 'GET' && pathname === '/api/events') {
|
|
305
636
|
handleApiPollEvents(res, router);
|
|
306
637
|
return;
|
|
@@ -316,8 +647,8 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
|
|
|
316
647
|
return;
|
|
317
648
|
}
|
|
318
649
|
|
|
319
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
320
|
-
|
|
650
|
+
if (req.method === 'POST' && pathname === '/api/canvas') {
|
|
651
|
+
handleApiCanvas(req, res, router, url);
|
|
321
652
|
return;
|
|
322
653
|
}
|
|
323
654
|
|
|
@@ -326,18 +657,18 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
|
|
|
326
657
|
return;
|
|
327
658
|
}
|
|
328
659
|
|
|
329
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
330
|
-
|
|
660
|
+
if (req.method === 'POST' && pathname === '/api/pty/create') {
|
|
661
|
+
handleApiPtyCreate(req, res);
|
|
331
662
|
return;
|
|
332
663
|
}
|
|
333
664
|
|
|
334
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
335
|
-
|
|
665
|
+
if (req.method === 'POST' && pathname === '/api/playground/exec') {
|
|
666
|
+
handleApiPlaygroundExec(req, res);
|
|
336
667
|
return;
|
|
337
668
|
}
|
|
338
669
|
|
|
339
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
340
|
-
|
|
670
|
+
if (req.method === 'POST' && pathname === '/api/workshop/setup') {
|
|
671
|
+
handleApiWorkshopSetup(req, res, router);
|
|
341
672
|
return;
|
|
342
673
|
}
|
|
343
674
|
|
|
@@ -346,29 +677,85 @@ function handleRequest(req, res, tierManager, router, options, chatSession, sess
|
|
|
346
677
|
return;
|
|
347
678
|
}
|
|
348
679
|
|
|
349
|
-
// ---
|
|
350
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
351
|
-
|
|
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
|
+
|
|
704
|
+
// --- Modules catalog ---
|
|
705
|
+
if (req.method === 'GET' && pathname === '/api/modules') {
|
|
706
|
+
handleApiModules(res);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// --- Journeys ---
|
|
711
|
+
if (req.method === 'GET' && pathname === '/api/journeys') {
|
|
712
|
+
handleApiJourneys(res, options);
|
|
352
713
|
return;
|
|
353
714
|
}
|
|
354
715
|
|
|
355
|
-
if (req.method === 'POST' && pathname === '/api/
|
|
356
|
-
|
|
716
|
+
if (req.method === 'POST' && pathname === '/api/journeys/activate') {
|
|
717
|
+
handleApiJourneyActivate(req, res, options);
|
|
357
718
|
return;
|
|
358
719
|
}
|
|
359
720
|
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
362
748
|
return;
|
|
363
749
|
}
|
|
364
750
|
|
|
365
|
-
|
|
366
|
-
|
|
751
|
+
// --- Settings ---
|
|
752
|
+
if (req.method === 'GET' && pathname === '/api/settings') {
|
|
753
|
+
handleApiSettingsGet(res);
|
|
367
754
|
return;
|
|
368
755
|
}
|
|
369
756
|
|
|
370
|
-
if (req.method === '
|
|
371
|
-
|
|
757
|
+
if (req.method === 'POST' && pathname === '/api/settings') {
|
|
758
|
+
handleApiSettingsPost(req, res);
|
|
372
759
|
return;
|
|
373
760
|
}
|
|
374
761
|
|
|
@@ -481,176 +868,817 @@ function handleApiCapture(req, res) {
|
|
|
481
868
|
}
|
|
482
869
|
|
|
483
870
|
/**
|
|
484
|
-
* GET /api/progress — return progress data.
|
|
871
|
+
* GET /api/progress — return progress data with achievements.
|
|
872
|
+
* Enriches module entries with title/description/difficulty from module.yaml catalog.
|
|
485
873
|
*/
|
|
486
874
|
function handleApiProgress(res, options) {
|
|
875
|
+
let progress;
|
|
487
876
|
if (options.progressProvider && typeof options.progressProvider.getProgress === 'function') {
|
|
488
|
-
|
|
489
|
-
sendJson(res, 200, progress);
|
|
877
|
+
progress = options.progressProvider.getProgress();
|
|
490
878
|
} else {
|
|
491
|
-
|
|
879
|
+
progress = { user: { xp: 0 }, modules: {} };
|
|
492
880
|
}
|
|
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
|
+
|
|
887
|
+
// Include achievements data from progress file
|
|
888
|
+
const achievements = progress.achievements || { unlocked: [], progress: {} };
|
|
889
|
+
const unlocked = (achievements.unlocked || []).map(a => ({
|
|
890
|
+
id: a.id,
|
|
891
|
+
date: a.date,
|
|
892
|
+
trigger: a.trigger,
|
|
893
|
+
...a,
|
|
894
|
+
}));
|
|
895
|
+
|
|
896
|
+
// Build near-locked list from progress counters + ACHIEVEMENTS criteria
|
|
897
|
+
const near = [];
|
|
898
|
+
const progressCounters = achievements.progress || {};
|
|
899
|
+
const unlockedIds = new Set(unlocked.map(a => a.id));
|
|
900
|
+
for (const def of ACHIEVEMENTS) {
|
|
901
|
+
if (unlockedIds.has(def.id)) continue;
|
|
902
|
+
const counter = progressCounters[def.id];
|
|
903
|
+
if (counter && counter.current > 0) {
|
|
904
|
+
near.push({
|
|
905
|
+
id: def.id,
|
|
906
|
+
icon: def.icon,
|
|
907
|
+
name: def.name,
|
|
908
|
+
description: def.description,
|
|
909
|
+
category: def.category,
|
|
910
|
+
current: counter.current,
|
|
911
|
+
target: counter.target || def.criteria?.target || 1,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
near.sort((a, b) => (b.current / b.target) - (a.current / a.target));
|
|
916
|
+
|
|
917
|
+
sendJson(res, 200, {
|
|
918
|
+
...progress,
|
|
919
|
+
achievements: { unlocked, near },
|
|
920
|
+
});
|
|
493
921
|
}
|
|
494
922
|
|
|
495
923
|
/**
|
|
496
|
-
* GET /api/
|
|
924
|
+
* GET /api/modules — return module catalog from built-in + installed modules.
|
|
497
925
|
*/
|
|
498
|
-
function
|
|
499
|
-
const
|
|
500
|
-
|
|
926
|
+
function handleApiModules(res) {
|
|
927
|
+
const modulesDir = resolve(__dirname, '..', '..', 'modules');
|
|
928
|
+
const modules = [];
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
if (existsSync(modulesDir)) {
|
|
932
|
+
const entries = readdirSync(modulesDir);
|
|
933
|
+
for (const entry of entries) {
|
|
934
|
+
const yamlPath = join(modulesDir, entry, 'module.yaml');
|
|
935
|
+
if (!existsSync(yamlPath)) continue;
|
|
936
|
+
try {
|
|
937
|
+
const raw = readFileSync(yamlPath, 'utf-8');
|
|
938
|
+
// Simple YAML parsing for module metadata (no dependency needed)
|
|
939
|
+
const meta = {};
|
|
940
|
+
for (const line of raw.split('\n')) {
|
|
941
|
+
const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
942
|
+
if (match) {
|
|
943
|
+
let val = match[2].trim();
|
|
944
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
945
|
+
if (val === 'true') val = true;
|
|
946
|
+
else if (val === 'false') val = false;
|
|
947
|
+
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
948
|
+
meta[match[1]] = val;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (meta.slug) {
|
|
952
|
+
const hasWorkshop = existsSync(join(modulesDir, entry, 'workshop.yaml'));
|
|
953
|
+
modules.push({
|
|
954
|
+
slug: meta.slug,
|
|
955
|
+
title: meta.title || meta.slug,
|
|
956
|
+
description: meta.description || '',
|
|
957
|
+
category: meta.category || 'general',
|
|
958
|
+
difficulty: meta.difficulty || 'beginner',
|
|
959
|
+
icon: meta.icon || null,
|
|
960
|
+
hasWorkshop,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
} catch {
|
|
964
|
+
// Skip unparseable modules
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
// Directory read failed
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
sendJson(res, 200, { modules });
|
|
501
973
|
}
|
|
502
974
|
|
|
503
975
|
/**
|
|
504
|
-
* GET /api/
|
|
505
|
-
* Holds the connection open until events arrive or timeout expires.
|
|
506
|
-
* Timeout is in seconds; defaults to 30, capped at MAX_LONG_POLL_TIMEOUT_MS.
|
|
976
|
+
* GET /api/journeys — return all journeys with resolved module status and progress.
|
|
507
977
|
*/
|
|
508
|
-
function
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
995
|
|
|
516
|
-
|
|
996
|
+
let progress = { modules: {}, journey: { active: null, progress: {} } };
|
|
997
|
+
if (options.progressProvider && typeof options.progressProvider.getProgress === 'function') {
|
|
998
|
+
progress = options.progressProvider.getProgress();
|
|
999
|
+
}
|
|
517
1000
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
// req 'close' fires when the request body is consumed (immediately after parsing),
|
|
521
|
-
// but res 'close' fires when the underlying connection is actually closed.
|
|
522
|
-
res.on('close', () => {
|
|
523
|
-
settled = true;
|
|
524
|
-
});
|
|
1001
|
+
const modulesDir = resolve(__dirname, '..', '..', 'modules');
|
|
1002
|
+
const moduleMap = buildModuleMap(modulesDir);
|
|
525
1003
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
1045
|
});
|
|
531
1046
|
}
|
|
532
1047
|
|
|
533
1048
|
/**
|
|
534
|
-
*
|
|
1049
|
+
* Build a map of module slugs to basic metadata from the modules directory.
|
|
535
1050
|
*/
|
|
536
|
-
function
|
|
537
|
-
|
|
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
|
+
}
|
|
538
1163
|
}
|
|
539
1164
|
|
|
540
1165
|
/**
|
|
541
|
-
*
|
|
542
|
-
* Used by the plugin to send canvas:* commands.
|
|
543
|
-
* REJECTS canvas:quiz messages — those MUST go through POST /api/quiz.
|
|
1166
|
+
* Resolve a journey's modules against the module map and progress.
|
|
544
1167
|
*/
|
|
545
|
-
function
|
|
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) {
|
|
546
1248
|
readBody(req, (err, body) => {
|
|
547
1249
|
if (err) {
|
|
548
1250
|
sendJson(res, 400, { error: 'Invalid request body' });
|
|
549
1251
|
return;
|
|
550
1252
|
}
|
|
551
1253
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
sendJson(res, 400, { error });
|
|
1254
|
+
if (!body || typeof body !== 'object') {
|
|
1255
|
+
sendJson(res, 400, { error: 'Body must be a JSON object' });
|
|
555
1256
|
return;
|
|
556
1257
|
}
|
|
557
1258
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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 });
|
|
564
1320
|
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
565
1323
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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 */ }
|
|
572
1347
|
}
|
|
573
1348
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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;
|
|
579
1360
|
}
|
|
580
1361
|
}
|
|
581
1362
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
}
|
|
585
1378
|
}
|
|
586
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
|
+
|
|
587
1398
|
/**
|
|
588
|
-
*
|
|
589
|
-
* Combines /api/visual + /api/events/wait into one atomic request so the caller
|
|
590
|
-
* cannot accidentally skip the poll step.
|
|
1399
|
+
* GET /api/module/:slug/resources/:filename — serve static resource files.
|
|
591
1400
|
*/
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
);
|
|
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
|
+
}
|
|
599
1469
|
|
|
1470
|
+
/**
|
|
1471
|
+
* PUT /api/sessions/:id — update session metadata.
|
|
1472
|
+
*/
|
|
1473
|
+
function handleApiSessionUpdate(req, res, sessionId) {
|
|
600
1474
|
readBody(req, (err, body) => {
|
|
601
1475
|
if (err) {
|
|
602
1476
|
sendJson(res, 400, { error: 'Invalid request body' });
|
|
603
1477
|
return;
|
|
604
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
|
+
}
|
|
605
1497
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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(() => {});
|
|
610
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
|
+
}
|
|
611
1513
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
1531
|
+
*/
|
|
1532
|
+
const SETTINGS_DIR = join(
|
|
1533
|
+
process.env.HOME || process.env.USERPROFILE || '/tmp',
|
|
1534
|
+
'.socrates'
|
|
1535
|
+
);
|
|
1536
|
+
const SETTINGS_FILE = join(SETTINGS_DIR, 'config.yaml');
|
|
1537
|
+
|
|
1538
|
+
const DEFAULT_SETTINGS = {
|
|
1539
|
+
theme: 'dark',
|
|
1540
|
+
accent: 'blue',
|
|
1541
|
+
chatPosition: 'left',
|
|
1542
|
+
fontSize: 14,
|
|
1543
|
+
model: 'claude-sonnet-4-6',
|
|
1544
|
+
animations: { confetti: true, transitions: true, diagrams: true },
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
function readSettings() {
|
|
1548
|
+
try {
|
|
1549
|
+
if (!existsSync(SETTINGS_FILE)) return { ...DEFAULT_SETTINGS };
|
|
1550
|
+
const raw = readFileSync(SETTINGS_FILE, 'utf-8');
|
|
1551
|
+
const settings = { ...DEFAULT_SETTINGS };
|
|
1552
|
+
for (const line of raw.split('\n')) {
|
|
1553
|
+
const match = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
1554
|
+
if (match) {
|
|
1555
|
+
let val = match[2].trim();
|
|
1556
|
+
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
|
|
1557
|
+
if (val === 'true') val = true;
|
|
1558
|
+
else if (val === 'false') val = false;
|
|
1559
|
+
else if (/^\d+$/.test(val)) val = parseInt(val, 10);
|
|
1560
|
+
settings[match[1]] = val;
|
|
617
1561
|
}
|
|
618
1562
|
}
|
|
1563
|
+
return settings;
|
|
1564
|
+
} catch {
|
|
1565
|
+
return { ...DEFAULT_SETTINGS };
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
619
1568
|
|
|
620
|
-
|
|
621
|
-
|
|
1569
|
+
function writeSettings(settings) {
|
|
1570
|
+
try {
|
|
1571
|
+
if (!existsSync(SETTINGS_DIR)) mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
1572
|
+
const lines = [];
|
|
1573
|
+
for (const [key, val] of Object.entries(settings)) {
|
|
1574
|
+
if (typeof val === 'object') {
|
|
1575
|
+
// Flatten nested objects (animations)
|
|
1576
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1577
|
+
lines.push(`${key}_${k}: ${v}`);
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
lines.push(`${key}: ${val}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
writeFileSync(SETTINGS_FILE, lines.join('\n') + '\n');
|
|
1584
|
+
} catch {
|
|
1585
|
+
// Ignore write errors
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
622
1588
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1589
|
+
/**
|
|
1590
|
+
* GET /api/settings — return canvas settings.
|
|
1591
|
+
*/
|
|
1592
|
+
function handleApiSettingsGet(res) {
|
|
1593
|
+
const settings = readSettings();
|
|
1594
|
+
sendJson(res, 200, settings);
|
|
1595
|
+
}
|
|
627
1596
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1597
|
+
/**
|
|
1598
|
+
* POST /api/settings — save canvas settings (partial update).
|
|
1599
|
+
*/
|
|
1600
|
+
function handleApiSettingsPost(req, res) {
|
|
1601
|
+
let body = '';
|
|
1602
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
1603
|
+
req.on('end', () => {
|
|
1604
|
+
try {
|
|
1605
|
+
const patch = JSON.parse(body);
|
|
1606
|
+
const current = readSettings();
|
|
1607
|
+
// Merge patch into current settings
|
|
1608
|
+
const updated = { ...current };
|
|
1609
|
+
for (const [key, val] of Object.entries(patch)) {
|
|
1610
|
+
if (key === 'animations' && typeof val === 'object') {
|
|
1611
|
+
updated.animations = { ...current.animations, ...val };
|
|
1612
|
+
} else if (DEFAULT_SETTINGS[key] !== undefined) {
|
|
1613
|
+
updated[key] = val;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
writeSettings(updated);
|
|
1617
|
+
sendJson(res, 200, updated);
|
|
1618
|
+
} catch {
|
|
1619
|
+
sendJson(res, 400, { error: 'Invalid JSON' });
|
|
1620
|
+
}
|
|
638
1621
|
});
|
|
639
1622
|
}
|
|
640
1623
|
|
|
641
1624
|
/**
|
|
642
|
-
*
|
|
643
|
-
* Mirrors /api/quiz: combines /api/visual + /api/events/wait into one atomic request
|
|
644
|
-
* so hooks cannot steal the event:game-result between the two calls.
|
|
1625
|
+
* GET /api/events — poll queued user events.
|
|
645
1626
|
*/
|
|
646
|
-
function
|
|
647
|
-
const
|
|
648
|
-
|
|
1627
|
+
function handleApiPollEvents(res, router) {
|
|
1628
|
+
const events = router.pollEvents();
|
|
1629
|
+
sendJson(res, 200, { events, count: events.length });
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* GET /api/events/wait?timeout=30 — long-poll for queued user events.
|
|
1634
|
+
* Holds the connection open until events arrive or timeout expires.
|
|
1635
|
+
* Timeout is in seconds; defaults to 30, capped at MAX_LONG_POLL_TIMEOUT_MS.
|
|
1636
|
+
*/
|
|
1637
|
+
function handleApiPollEventsWait(req, res, router, url) {
|
|
1638
|
+
const timeoutParam = parseInt(url.searchParams.get('timeout') || '30', 10);
|
|
1639
|
+
const timeoutSec = isNaN(timeoutParam) ? 30 : timeoutParam;
|
|
649
1640
|
const timeoutMs = Math.min(
|
|
650
1641
|
Math.max(timeoutSec * 1000, 1000),
|
|
651
1642
|
MAX_LONG_POLL_TIMEOUT_MS
|
|
652
1643
|
);
|
|
653
1644
|
|
|
1645
|
+
let settled = false;
|
|
1646
|
+
|
|
1647
|
+
// Handle client disconnect before timeout.
|
|
1648
|
+
// IMPORTANT: Use res.on('close'), NOT req.on('close').
|
|
1649
|
+
// req 'close' fires when the request body is consumed (immediately after parsing),
|
|
1650
|
+
// but res 'close' fires when the underlying connection is actually closed.
|
|
1651
|
+
res.on('close', () => {
|
|
1652
|
+
settled = true;
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
router.pollEventsAsync(timeoutMs).then((events) => {
|
|
1656
|
+
if (settled) return; // Client already disconnected
|
|
1657
|
+
settled = true;
|
|
1658
|
+
sendJson(res, 200, { events, count: events.length });
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* GET /api/templates — list available templates.
|
|
1664
|
+
*/
|
|
1665
|
+
function handleApiTemplates(res) {
|
|
1666
|
+
sendJson(res, 200, { templates: listTemplates() });
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* POST /api/canvas — unified 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 } }
|
|
1680
|
+
*/
|
|
1681
|
+
function handleApiCanvas(req, res, router, url) {
|
|
654
1682
|
readBody(req, (err, body) => {
|
|
655
1683
|
if (err) {
|
|
656
1684
|
sendJson(res, 400, { error: 'Invalid request body' });
|
|
@@ -663,7 +1691,6 @@ function handleApiGame(req, res, router, url) {
|
|
|
663
1691
|
return;
|
|
664
1692
|
}
|
|
665
1693
|
|
|
666
|
-
// Render template if needed (same as /api/visual)
|
|
667
1694
|
if (envelope.payload && envelope.payload.template) {
|
|
668
1695
|
const rendered = renderTemplate(envelope.payload.template, envelope.payload.data || {});
|
|
669
1696
|
if (rendered) {
|
|
@@ -671,13 +1698,20 @@ function handleApiGame(req, res, router, url) {
|
|
|
671
1698
|
}
|
|
672
1699
|
}
|
|
673
1700
|
|
|
674
|
-
// Step 1: Send the game to connected clients
|
|
675
1701
|
router.routeVisualCommand(envelope);
|
|
676
1702
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1703
|
+
const awaitSpec = envelope.await;
|
|
1704
|
+
if (!awaitSpec || !awaitSpec.event) {
|
|
1705
|
+
sendJson(res, 200, { ok: true, tier: router.tierManager.getTier() });
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
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
|
+
);
|
|
1714
|
+
|
|
681
1715
|
let settled = false;
|
|
682
1716
|
res.on('close', () => { settled = true; });
|
|
683
1717
|
|
|
@@ -767,18 +1801,7 @@ function handleApiOpenExtension(res) {
|
|
|
767
1801
|
return;
|
|
768
1802
|
}
|
|
769
1803
|
|
|
770
|
-
|
|
771
|
-
if (platform === 'darwin') {
|
|
772
|
-
exec(`open "${extensionDir}"`);
|
|
773
|
-
exec('open -a "Google Chrome" "chrome://extensions"');
|
|
774
|
-
} else if (platform === 'linux') {
|
|
775
|
-
exec(`xdg-open "${extensionDir}"`);
|
|
776
|
-
exec('google-chrome "chrome://extensions" 2>/dev/null || chromium-browser "chrome://extensions" 2>/dev/null');
|
|
777
|
-
} else if (platform === 'win32') {
|
|
778
|
-
exec(`explorer "${extensionDir}"`);
|
|
779
|
-
exec('start chrome "chrome://extensions"');
|
|
780
|
-
}
|
|
781
|
-
|
|
1804
|
+
// Return the path only — no longer auto-opens Finder/Chrome.
|
|
782
1805
|
sendJson(res, 200, { ok: true, path: extensionDir });
|
|
783
1806
|
}
|
|
784
1807
|
|
|
@@ -829,101 +1852,126 @@ function handleApiTerminalExec(req, res, router) {
|
|
|
829
1852
|
});
|
|
830
1853
|
}
|
|
831
1854
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1855
|
+
/**
|
|
1856
|
+
* POST /api/pty/create — create a new PTY terminal session.
|
|
1857
|
+
*/
|
|
1858
|
+
function handleApiPtyCreate(req, res) {
|
|
835
1859
|
readBody(req, async (err, body) => {
|
|
836
|
-
if (err)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1860
|
+
if (err) {
|
|
1861
|
+
sendJson(res, 400, { error: 'Invalid request body' });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
841
1864
|
try {
|
|
842
|
-
const sessionId = await
|
|
843
|
-
cwd: body?.cwd
|
|
844
|
-
|
|
845
|
-
|
|
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,
|
|
846
1871
|
});
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
sendJson(res, 200, { ok: true, sessionId });
|
|
851
|
-
} catch (startErr) {
|
|
852
|
-
sendJson(res, 500, { error: startErr.message });
|
|
1872
|
+
sendJson(res, 200, { sessionId, cwd });
|
|
1873
|
+
} catch (e) {
|
|
1874
|
+
sendJson(res, 500, { error: e.message });
|
|
853
1875
|
}
|
|
854
1876
|
});
|
|
855
1877
|
}
|
|
856
1878
|
|
|
857
|
-
|
|
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) {
|
|
858
1884
|
readBody(req, (err, body) => {
|
|
859
|
-
if (err
|
|
860
|
-
sendJson(res, 400, { error: '
|
|
1885
|
+
if (err) {
|
|
1886
|
+
sendJson(res, 400, { error: 'Invalid request body' });
|
|
861
1887
|
return;
|
|
862
1888
|
}
|
|
863
1889
|
|
|
864
|
-
const
|
|
865
|
-
if (
|
|
866
|
-
sendJson(res, 400, { error: '
|
|
1890
|
+
const { code, language } = body || {};
|
|
1891
|
+
if (typeof code !== 'string') {
|
|
1892
|
+
sendJson(res, 400, { error: 'Missing "code" string in request body' });
|
|
867
1893
|
return;
|
|
868
1894
|
}
|
|
869
1895
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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;
|
|
884
1919
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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 });
|
|
889
1936
|
});
|
|
890
1937
|
});
|
|
891
1938
|
}
|
|
892
1939
|
|
|
893
|
-
|
|
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) {
|
|
894
1945
|
readBody(req, async (err, body) => {
|
|
895
|
-
if (err
|
|
896
|
-
sendJson(res, 400, { error: '
|
|
1946
|
+
if (err) {
|
|
1947
|
+
sendJson(res, 400, { error: 'Invalid request body' });
|
|
897
1948
|
return;
|
|
898
1949
|
}
|
|
899
1950
|
|
|
900
|
-
const clientId = body?.clientId;
|
|
901
|
-
|
|
902
1951
|
try {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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,
|
|
907
1958
|
});
|
|
908
|
-
|
|
909
|
-
|
|
1959
|
+
|
|
1960
|
+
if (files && Array.isArray(files)) {
|
|
1961
|
+
await writeSessionFiles(sessionId, files);
|
|
910
1962
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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 });
|
|
914
1971
|
}
|
|
915
1972
|
});
|
|
916
1973
|
}
|
|
917
1974
|
|
|
918
|
-
async function handleApiChatSessions(res, chatSession) {
|
|
919
|
-
try {
|
|
920
|
-
const sessions = await chatSession.listSessions(process.cwd());
|
|
921
|
-
sendJson(res, 200, { sessions });
|
|
922
|
-
} catch (listErr) {
|
|
923
|
-
sendJson(res, 500, { error: listErr.message });
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
1975
|
// --- Helpers ---
|
|
928
1976
|
|
|
929
1977
|
function readBody(req, callback) {
|
|
@@ -956,8 +2004,212 @@ function ensureDir(dirPath) {
|
|
|
956
2004
|
}
|
|
957
2005
|
}
|
|
958
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
|
+
|
|
959
2200
|
// --- Standalone execution ---
|
|
960
2201
|
if (process.argv[1] && process.argv[1].includes('bridge/src/server.js')) {
|
|
2202
|
+
// Prevent unhandled errors from crashing the server
|
|
2203
|
+
process.on('uncaughtException', (err) => {
|
|
2204
|
+
console.error('[bridge] Uncaught exception (server kept alive):', err.message);
|
|
2205
|
+
});
|
|
2206
|
+
process.on('unhandledRejection', (err) => {
|
|
2207
|
+
console.error('[bridge] Unhandled rejection (server kept alive):', err?.message || err);
|
|
2208
|
+
});
|
|
2209
|
+
|
|
961
2210
|
const port = parseInt(process.env.PORT || process.argv[2] || DEFAULT_PORT, 10);
|
|
962
|
-
|
|
2211
|
+
const pluginDir = resolve(__dirname, '..', '..', 'plugin');
|
|
2212
|
+
const progressFile = resolve(process.cwd(), '.socrates-progress.yaml');
|
|
2213
|
+
const progressProvider = createFileProgressProvider(progressFile);
|
|
2214
|
+
startServer({ port, pluginDir, progressProvider });
|
|
963
2215
|
}
|