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