mulmoclaude 0.9.0 → 0.9.2
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 +95 -38
- package/bin/mulmoclaude.js +1 -2
- package/client/assets/JsonEditor-CPJfn76E.css +1 -0
- package/client/assets/JsonEditor-CoWGJG3Y.js +10 -0
- package/client/assets/abnfDiagram-VRR7QNED-6nNByj6v-By7GJ3xW.js +1 -0
- package/client/assets/abnfDiagram-VRR7QNED-BrQlVixL.js +1 -0
- package/client/assets/arc-BXutyUAX.js +1 -0
- package/client/assets/arc-zT0wd74V-DulGJNnb.js +1 -0
- package/client/assets/architecture-TIHT7OUA-CYMWc3UT-C7HoFzWN.js +1 -0
- package/client/assets/architecture-TIHT7OUA-aml8u-G9.js +1 -0
- package/client/assets/architectureDiagram-ZJ3FMSHR-Bgnyaj_i-CCpEsCHN.js +36 -0
- package/client/assets/architectureDiagram-ZJ3FMSHR-DJTFpPjB.js +36 -0
- package/client/assets/array-BifhSqXX.js +1 -0
- package/client/assets/array-CApNbSU9-BifhSqXX.js +1 -0
- package/client/assets/blockDiagram-677ZJIJ3-DQ35o5E4-BXS11D3J.js +132 -0
- package/client/assets/blockDiagram-677ZJIJ3-aYvCODxv.js +132 -0
- package/client/assets/c4Diagram-LMCZKHZV-Bau6RMBb.js +10 -0
- package/client/assets/c4Diagram-LMCZKHZV-ClWZeiWo-DXGnOOUf.js +10 -0
- package/client/assets/channel-D9VSi_QV.js +1 -0
- package/client/assets/channel-Di5rtkx0-C3gZcytF.js +1 -0
- package/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/client/assets/chunk-2Q5K7J3B-DfKpwyl9-DTxwf5E9.js +1 -0
- package/client/assets/chunk-32BRIVSS-CCt9wtYd-Cwa_IP35.js +1 -0
- package/client/assets/chunk-32BRIVSS-CxzHeys5.js +1 -0
- package/client/assets/chunk-52WLFC77-BtNjbTdw.js +10 -0
- package/client/assets/chunk-52WLFC77-FbBbR4uI-wsNC5d-f.js +10 -0
- package/client/assets/chunk-5VM5RSS4-BYnnUiN6-C4rOnu9A.js +15 -0
- package/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/client/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- package/client/assets/chunk-7BUUIJ7U-hh8aCuGX-mGEpPzQy.js +1 -0
- package/client/assets/chunk-C7G6YPKG-BE8ehsnc.js +1 -0
- package/client/assets/chunk-C7G6YPKG-C87hlS9c-BitbJpTa.js +1 -0
- package/client/assets/{chunk-D8eiyYIV-BY16KEZc.js → chunk-D8eiyYIV-BeeEsA1B.js} +1 -1
- package/client/assets/chunk-EX3LRPZG-Bhgskzyy.js +231 -0
- package/client/assets/chunk-EX3LRPZG-BqGqMXLN-Cl2Eo1_u.js +231 -0
- package/client/assets/chunk-FWX5IMBZ-BkbSAAuW-C2r4AuAO.js +2 -0
- package/client/assets/chunk-FWX5IMBZ-D9sskZb_.js +2 -0
- package/client/assets/chunk-HOUHSVGY-CBCXk7rb.js +1 -0
- package/client/assets/chunk-HOUHSVGY-Cxu0eDlh-owg_DcY0.js +1 -0
- package/client/assets/chunk-ICXQ74PX-DTqPpDQ9.js +2 -0
- package/client/assets/chunk-ICXQ74PX-hiraF_Xj-CdaEwVin.js +2 -0
- package/client/assets/chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js +1 -0
- package/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/client/assets/chunk-KEIR6QF5-DNzq6p3w.js +161 -0
- package/client/assets/chunk-MOJQB5TN-CqxshQHA-Cn_EpDyZ.js +88 -0
- package/client/assets/chunk-MOJQB5TN-D1s_zUGz.js +88 -0
- package/client/assets/chunk-OGEWGWER-CjCr7ceX-Dfk5-70y.js +1 -0
- package/client/assets/chunk-OGEWGWER-DzfhxoQd.js +1 -0
- package/client/assets/chunk-PUDLZKDR-BZlebNhn.js +156 -0
- package/client/assets/chunk-PUDLZKDR-Dx6M-vz1-CLyVurOz.js +156 -0
- package/client/assets/chunk-Q4XR5HBZ-BcJb7mwA.js +70 -0
- package/client/assets/chunk-Q4XR5HBZ-CZd-9lTB-qPEYoRNc.js +11 -0
- package/client/assets/chunk-RYQCIY6F-B_IgMG7T.js +1 -0
- package/client/assets/chunk-RYQCIY6F-YkbemkCt-CX4fncTQ.js +1 -0
- package/client/assets/chunk-V7JOEXUC-Buce04o6.js +206 -0
- package/client/assets/chunk-V7JOEXUC-rI0xlC_O-C607vJXy.js +206 -0
- package/client/assets/chunk-VAUOI2AC-CxhkzXN0.js +1 -0
- package/client/assets/chunk-VAUOI2AC-DrcykVNK-CSROJWdE.js +1 -0
- package/client/assets/chunk-VR4S4FIN-D5afNrsS.js +1 -0
- package/client/assets/chunk-VR4S4FIN-O6iF8Yvf-CLIwrgqU.js +1 -0
- package/client/assets/chunk-WYO6CB5R-B83L_z6I-Cs8mg9If.js +127 -0
- package/client/assets/chunk-WYO6CB5R-BXTIdTMw.js +125 -0
- package/client/assets/chunk-XXDRQBXY-DFXH_lWn-B5HjCp2c.js +1 -0
- package/client/assets/chunk-XXDRQBXY-DXMD6ofA.js +1 -0
- package/client/assets/chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js +1 -0
- package/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/client/assets/chunk-ZGVPDNZ5-BpFv9JSP-CHYySMjV.js +62 -0
- package/client/assets/chunk-ZGVPDNZ5-CYusI0J_.js +62 -0
- package/client/assets/chunk-ZIRB5QZD-Biy6ZNFG-D1maAiXb.js +32 -0
- package/client/assets/chunk-ZIRB5QZD-C6fEPe3t.js +32 -0
- package/client/assets/classDiagram-OUVF2IWQ-BgAZMSbT-BgxEE1D3.js +1 -0
- package/client/assets/classDiagram-OUVF2IWQ-CBD3Eidl.js +1 -0
- package/client/assets/classDiagram-v2-EOCWNBFH-CBD3Eidl.js +1 -0
- package/client/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-BgxEE1D3.js +1 -0
- package/client/assets/cose-bilkent-JH36ORCC-CuSL2tA8-aMIqISfj.js +1 -0
- package/client/assets/cose-bilkent-JH36ORCC-WAJDBXv1.js +1 -0
- package/client/assets/cynefin-VYW2F7L2-DH2qkJKw.js +1 -0
- package/client/assets/cynefin-VYW2F7L2-DqA3n9nY-BNWhIK_n.js +1 -0
- package/client/assets/cynefinDiagram-TSTJHNR4-CUvPV9MV.js +62 -0
- package/client/assets/cynefinDiagram-TSTJHNR4-x0-0sQ15-Cq1gFcFu.js +62 -0
- package/client/assets/cytoscape.esm-CzdwbRaj-Djp6vQyU.js +321 -0
- package/client/assets/cytoscape.esm-Djp6vQyU.js +321 -0
- package/client/assets/dagre-CXRCoUWR.js +1 -0
- package/client/assets/dagre-VKFMJZFB-CQdfl-bx-CzZZuace.js +4 -0
- package/client/assets/dagre-VKFMJZFB-C_9IS7mB.js +4 -0
- package/client/assets/dagre-wczQIDso-CfevN9RO.js +1 -0
- package/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/client/assets/defaultLocale-DUNguUWs-BbDo_yJX.js +1 -0
- package/client/assets/diagram-FQU43EPY-BOSB6VUb-isPdQ8WX.js +3 -0
- package/client/assets/diagram-FQU43EPY-BP8N00-b.js +3 -0
- package/client/assets/diagram-G47NLZAW-DLXrcXsN-BIqj7RKy.js +24 -0
- package/client/assets/diagram-G47NLZAW-ulE1JlWG.js +24 -0
- package/client/assets/diagram-NH7WQ7WH-BMQp1rkF-D6-fOq_v.js +24 -0
- package/client/assets/diagram-NH7WQ7WH-D_IXrL3i.js +24 -0
- package/client/assets/diagram-OA4YK3LP-C-pC6Eyu.js +30 -0
- package/client/assets/diagram-OA4YK3LP-D1wQ0vUj-BNi6QLCK.js +30 -0
- package/client/assets/diagram-WEI45ONY-Di9m35i-.js +41 -0
- package/client/assets/diagram-WEI45ONY-RR0DpF8R-BS3nrb3w.js +41 -0
- package/client/assets/dist-BQBs5pjy-BmtZ7Oc2.js +1 -0
- package/client/assets/dist-CQ3HaWOk.js +1 -0
- package/client/assets/ebnfDiagram-CCIWWBDH-Cbwnim2x.js +1 -0
- package/client/assets/ebnfDiagram-CCIWWBDH-M123uVJ8-9-dq55nQ.js +1 -0
- package/client/assets/erDiagram-Q63AITRT-BWx_-PXG-CWGfG4z5.js +85 -0
- package/client/assets/erDiagram-Q63AITRT-CMbtO3Sm.js +85 -0
- package/client/assets/eventmodeling-45OFAUF4--32SIpkL.js +1 -0
- package/client/assets/eventmodeling-45OFAUF4-_BVSjAXf-DQdL0Icr.js +1 -0
- package/client/assets/flowDiagram-23GEKE2U-BeOc_anm-CuVzKmmU.js +1 -0
- package/client/assets/flowDiagram-23GEKE2U-Dofa_qxG.js +1 -0
- package/client/assets/ganttDiagram-NO4QXBWP-BOoJ1eTw-TWoNmSvq.js +292 -0
- package/client/assets/ganttDiagram-NO4QXBWP-BgoAVKuc.js +292 -0
- package/client/assets/gitGraph-TEB2WS4Q-CH12KLTN-Bz4frAhV.js +1 -0
- package/client/assets/gitGraph-TEB2WS4Q-DIMvNvqt.js +1 -0
- package/client/assets/gitGraphDiagram-IHSO6WYX-B2CJhk_G-ClOKkjxw.js +106 -0
- package/client/assets/gitGraphDiagram-IHSO6WYX-Dmb6KnPz.js +106 -0
- package/client/assets/graphlib-B8gBHxth.js +1 -0
- package/client/assets/graphlib-hY-1btwe-DQjxxcnr.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-CKJ6vKPo.js → html2canvas-CDGcmOD3-DRL9pFVl.js} +2 -2
- package/client/assets/{index-9lmYSaus.js → index-Dc0R-HW5.js} +129 -190
- package/client/assets/index-Dxo1Zdd-.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DFXjJgCa.js → index.es-DqtpmBm8-EQk3NgR8.js} +1 -1
- package/client/assets/info-DKCQHKI2-Cbw3mbiK-GsDZz9IO.js +1 -0
- package/client/assets/info-DKCQHKI2-ViCqobGo.js +1 -0
- package/client/assets/infoDiagram-FWYZ7A6U-HKV7LIG-.js +2 -0
- package/client/assets/infoDiagram-FWYZ7A6U-Mp1X3pBP-ATJv87Ur.js +2 -0
- package/client/assets/init-D6jRqBbL.js +1 -0
- package/client/assets/init-DEsX3bhM-D6jRqBbL.js +1 -0
- package/client/assets/ishikawaDiagram-FXEZZL3T-BNG7tkJu-CGCeOIlJ.js +70 -0
- package/client/assets/ishikawaDiagram-FXEZZL3T-CVDUj46f.js +70 -0
- package/client/assets/journeyDiagram-5HDEW3XC-BA-ESGLP.js +139 -0
- package/client/assets/journeyDiagram-5HDEW3XC-Dbp_hY9X-Bx-IoCe9.js +139 -0
- package/client/assets/kanban-definition-HUTT4EX6-Bqaet01L.js +89 -0
- package/client/assets/kanban-definition-HUTT4EX6-DSTc5u3q-C0JGckt1.js +89 -0
- package/client/assets/katex-CddkPoXu.js +257 -0
- package/client/assets/katex-M0IxphGf-CddkPoXu.js +257 -0
- package/client/assets/lib-4Tgx7rAy.js +114 -0
- package/client/assets/line-B1wBwzrY-f5wxpqoF.js +1 -0
- package/client/assets/line-CkyHfW7d.js +1 -0
- package/client/assets/linear-B9fuEF4c.js +1 -0
- package/client/assets/linear-lrGinF5_-CqNBync7.js +1 -0
- package/client/assets/map-CgrwEyH7-DWQFomlZ.js +1 -0
- package/client/assets/map-DsCK-0Cs.js +1 -0
- package/client/assets/marked.esm-CIU5FDdN.js +64 -0
- package/client/assets/marp-CSq0PPfK.js +3448 -0
- package/client/assets/material-symbols-outlined-DdRFxZLh.woff2 +0 -0
- package/client/assets/mermaid-parser.core-DC7NPJ_M-Ca6XzwfM.js +166 -0
- package/client/assets/mermaid-parser.core-SAwSf4_o.js +7 -0
- package/client/assets/mermaid.core-BBEQfkdJ.js +11 -0
- package/client/assets/mermaid.core-DZM3Ha-E-BxOo-RYG.js +11 -0
- package/client/assets/mindmap-definition-LN4V7U3C-B4BN0efL.js +96 -0
- package/client/assets/mindmap-definition-LN4V7U3C-DYtgcMsY-DynyLTNv.js +96 -0
- package/client/assets/ordinal-B9_Llu10-CU85fhaB.js +1 -0
- package/client/assets/ordinal-CopWnP7w.js +1 -0
- package/client/assets/packet-7NZHBO7P-CY4XRk9N.js +1 -0
- package/client/assets/packet-7NZHBO7P-lwb58iYx--vqSg65J.js +1 -0
- package/client/assets/path-BWPyau1x.js +1 -0
- package/client/assets/path-COivZ1Gw-C5riojK2.js +1 -0
- package/client/assets/pegDiagram-2B236MQR-BOKErjUu.js +1 -0
- package/client/assets/pegDiagram-2B236MQR-C43eIpKM-Bi_5Sxjt.js +1 -0
- package/client/assets/pie-RZYD4A2V-6BHB7bAx.js +1 -0
- package/client/assets/pie-RZYD4A2V-B1UWb4Gu-Dmn2hLWH.js +1 -0
- package/client/assets/pieDiagram-ENE6RG2P-BkTqgJyR-Bcdm19kR.js +39 -0
- package/client/assets/pieDiagram-ENE6RG2P-DtNjvz-U.js +39 -0
- package/client/assets/preload-helper-CZgWQFsJ.js +1 -0
- package/client/assets/purify.es-DY32g7DN.js +3 -0
- package/client/assets/quadrantDiagram-ABIIQ3AL-BQmAJL2v.js +7 -0
- package/client/assets/quadrantDiagram-ABIIQ3AL-Bm1Zjm45-B3orqx_f.js +7 -0
- package/client/assets/radar-I7S5WNFK-7CKcb_l--CqKdnR0v.js +1 -0
- package/client/assets/radar-I7S5WNFK-R7Jhjfdt.js +1 -0
- package/client/assets/railroad-3IZDKUUU-BepSPyHI.js +1 -0
- package/client/assets/railroad-3IZDKUUU-gCySKdnW-CKU26HTP.js +1 -0
- package/client/assets/railroad-abnf-AHOZXSZD-BlsHsp3c.js +1 -0
- package/client/assets/railroad-abnf-AHOZXSZD-BophH4r--CGkGHq54.js +1 -0
- package/client/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-B7bn_PF3.js +1 -0
- package/client/assets/railroad-ebnf-EBAXGLYW-B4fMcGEH.js +1 -0
- package/client/assets/railroad-peg-LSFZ7HO6-B1ZrjJIu.js +1 -0
- package/client/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-CP_B-N5g.js +1 -0
- package/client/assets/railroadDiagram-RFXS5EU6-8cz0Iby3.js +1 -0
- package/client/assets/railroadDiagram-RFXS5EU6-BkfbdeAs-Di6jH5I6.js +1 -0
- package/client/assets/requirementDiagram-TGXJPOKE-CrGTTjYg-DmIC7Shp.js +84 -0
- package/client/assets/requirementDiagram-TGXJPOKE-Dlqoe9TY.js +84 -0
- package/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/client/assets/rough.esm-wVmwlXu7-CSKSodPl.js +1 -0
- package/client/assets/runtime-protocol-vue-WG3JLsjz.js +1 -0
- package/client/assets/runtime-vue-lHsY5zMj.js +1 -0
- package/client/assets/sankeyDiagram-HTMAVEWB-BkTKgu8Q.js +40 -0
- package/client/assets/sankeyDiagram-HTMAVEWB-rWXPf03Z-CjV9pCSJ.js +40 -0
- package/client/assets/{schemas-D_RbFtuQ.js → schemas-DGvl73AE.js} +3 -3
- package/client/assets/sequenceDiagram-DBY2YBRQ-BpuXUcFy.js +162 -0
- package/client/assets/sequenceDiagram-DBY2YBRQ-nkJYWO2m-BqS_sTCH.js +162 -0
- package/client/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- package/client/assets/sizeCapture-X5ZJPWSS-F5SadAuT-VHIb4_NU.js +1 -0
- package/client/assets/src-Brzfja-q-DNc4Or4z.js +1 -0
- package/client/assets/src-Cqa3Jy1j.js +1 -0
- package/client/assets/stateDiagram-2N3HPSRC-CdlGXuo7.js +1 -0
- package/client/assets/stateDiagram-2N3HPSRC-T4-clK8b-DQRiB4va.js +1 -0
- package/client/assets/stateDiagram-v2-6OUMAXLB-BN8Jr3ZM.js +1 -0
- package/client/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-DbrOhUn_.js +1 -0
- package/client/assets/swimlanes-5IMT3BWC-HmQNEntu-CNBgZdur.js +2 -0
- package/client/assets/swimlanes-5IMT3BWC-iEJg5gki.js +2 -0
- package/client/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-DpqgeNRA.js +8 -0
- package/client/assets/swimlanesDiagram-G3AALYLV-o-sS6aEP.js +8 -0
- package/client/assets/timeline-definition-FHXFAJF6-Bjh-7QmL.js +120 -0
- package/client/assets/timeline-definition-FHXFAJF6-CNc9jSTP-BKNSymgY.js +120 -0
- package/client/assets/treeView-QDETBFTQ-D3cqRl6k.js +1 -0
- package/client/assets/treeView-QDETBFTQ-nIQcG1h9-O_ri9HxD.js +1 -0
- package/client/assets/treemap-6X3UGDF4-BnsvC8yL-Cfsty1e3.js +1 -0
- package/client/assets/treemap-6X3UGDF4-DGkZcvDG.js +1 -0
- package/client/assets/v4-B29ayslu.js +1 -0
- package/client/assets/vennDiagram-L72KCM5P-Cz3dbe7b.js +34 -0
- package/client/assets/vennDiagram-L72KCM5P-Dr3pTJ_0-C38xwcVo.js +34 -0
- package/client/assets/vue-C6d2VveO.js +1 -0
- package/client/assets/vue-i18n-CL-ejuNu.js +3 -0
- package/client/assets/vue.runtime.esm-bundler-BeoTfMNc.js +4 -0
- package/client/assets/wardley-OPB4EBWU-8Odxkx6V-CCDIVuBs.js +1 -0
- package/client/assets/wardley-OPB4EBWU-CZsCDvKk.js +1 -0
- package/client/assets/wardleyDiagram-EHGQE667-DSFc7ZZa-mlWXRiWx.js +78 -0
- package/client/assets/wardleyDiagram-EHGQE667-DiyoyO_X.js +78 -0
- package/client/assets/xychartDiagram-FW5EYKEG-BP0Nn4Pp-DRRiStGZ.js +7 -0
- package/client/assets/xychartDiagram-FW5EYKEG-DhY1_UGM.js +7 -0
- package/client/index.html +16 -12
- package/package.json +17 -19
- package/server/agent/backend/types.ts +1 -1
- package/server/agent/backgroundSessions.ts +43 -0
- package/server/agent/config.ts +3 -2
- package/server/agent/mcp-tools/manageCollection.ts +4 -4
- package/server/agent/sandboxMounts.ts +1 -1
- package/server/agent/stream.ts +1 -3
- package/server/api/auth/viewToken.ts +1 -1
- package/server/api/bridge/sessionRole.ts +47 -0
- package/server/api/routes/agent.ts +79 -2
- package/server/api/routes/collections.ts +60 -7
- package/server/api/routes/collectionsRegistry.ts +113 -0
- package/server/api/routes/dashboard.ts +47 -0
- package/server/api/routes/feeds.ts +2 -2
- package/server/api/routes/files.ts +1 -2
- package/server/api/routes/pdf.ts +48 -9
- package/server/api/routes/plugins.ts +2 -2
- package/server/api/routes/remoteHost.ts +55 -0
- package/server/api/routes/sessions.ts +20 -11
- package/server/api/routes/share.ts +105 -0
- package/server/api/routes/transcribe.ts +1 -1
- package/server/api/routes/wiki/history.ts +2 -2
- package/server/api/routes/wiki.ts +29 -177
- package/server/api/sandboxStatus.ts +1 -1
- package/server/build/dispatcher.mjs +25 -14928
- package/server/events/collection-change.ts +4 -4
- package/server/events/file-change.ts +2 -2
- package/server/events/scheduler-adapter.ts +3 -3
- package/server/events/task-manager/index.ts +3 -3
- package/server/index.ts +53 -17
- package/server/notifier/engine.ts +3 -3
- package/server/notifier/types.ts +2 -2
- package/server/plugins/html-builtin.ts +15 -1
- package/server/prompts/system/system.md +4 -0
- package/server/remoteHost/auth.ts +29 -0
- package/server/remoteHost/commandChannel.ts +56 -0
- package/server/remoteHost/firebase.ts +16 -0
- package/server/remoteHost/handlers/collectionPage.ts +29 -0
- package/server/remoteHost/handlers/getCollection.ts +32 -0
- package/server/remoteHost/handlers/getFeed.ts +35 -0
- package/server/remoteHost/handlers/index.ts +19 -0
- package/server/remoteHost/handlers/listCollections.ts +29 -0
- package/server/remoteHost/handlers/listFeeds.ts +37 -0
- package/server/remoteHost/handlers/listShortcuts.ts +24 -0
- package/server/remoteHost/handlers/startChat.ts +78 -0
- package/server/remoteHost/hostRunner.ts +122 -0
- package/server/remoteHost/index.ts +117 -0
- package/server/system/config.ts +1 -1
- package/server/system/credentials.ts +2 -3
- package/server/system/docker.ts +9 -8
- package/server/system/env.ts +8 -0
- package/server/system/whisper/index.ts +4 -4
- package/server/utils/claudeConfigPath.ts +57 -0
- package/server/utils/files/dashboard-io.ts +87 -0
- package/server/utils/files/session-io.ts +11 -0
- package/server/utils/markdown/frontmatter.ts +8 -4
- package/server/utils/regex.ts +4 -3
- package/server/utils/share/packHtml.ts +108 -0
- package/server/utils/share/rewriteAssets.ts +178 -0
- package/server/workspace/chat-index/paths.ts +8 -2
- package/server/workspace/chat-index/summarizer.ts +23 -15
- package/server/workspace/collections/configure.ts +5 -3
- package/server/workspace/collections/index.ts +8 -6
- package/server/workspace/collections/notifications.ts +3 -3
- package/server/workspace/collections/types.ts +2 -2
- package/server/workspace/collections/watcher.ts +2 -2
- package/server/workspace/feeds/configure.ts +24 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +2 -2
- package/server/workspace/hooks/handlers/wikiSnapshot.ts +1 -1
- package/server/workspace/paths.ts +8 -1
- package/server/workspace/skills/catalog.ts +1 -3
- package/server/workspace/skills/external/catalog.ts +1 -3
- package/server/workspace/skills/paths.ts +4 -3
- package/server/workspace/skills/writer.ts +1 -4
- package/server/workspace/skills-preset.ts +3 -3
- package/server/workspace/wiki-pages/io.ts +2 -2
- package/server/workspace/workspace.ts +2 -2
- package/src/App.vue +41 -18
- package/src/components/ChatInput.vue +1 -1
- package/src/components/DashboardView.vue +372 -0
- package/src/components/FileContentRenderer.vue +32 -7
- package/src/components/JsonEditor.vue +1 -1
- package/src/components/PluginLauncher.vue +122 -79
- package/src/components/RemoteHostControl.vue +159 -0
- package/src/components/SessionCountBadges.vue +32 -0
- package/src/components/SessionHeaderControls.vue +1 -8
- package/src/components/SessionHistoryPanel.vue +2 -0
- package/src/components/SessionHistoryToggleButton.vue +1 -15
- package/src/components/SidebarHeader.vue +2 -0
- package/src/components/collectionTypes.ts +2 -2
- package/src/composables/accountingHost.ts +24 -0
- package/src/composables/collections/uiHost.ts +17 -2
- package/src/composables/useDashboard.ts +191 -0
- package/src/composables/useMarkdownZip.ts +51 -0
- package/src/composables/useNotifications.ts +1 -4
- package/src/composables/useSharePack.ts +60 -0
- package/src/composables/useTranslatedQueries.ts +15 -95
- package/src/composables/useTranslatedStrings.ts +68 -0
- package/src/composables/useVoiceInput.ts +3 -3
- package/src/config/apiRoutes.ts +57 -9
- package/src/config/createFilePolicy.ts +1 -1
- package/src/config/firebase.ts +12 -0
- package/src/config/firebaseConfig.ts +18 -0
- package/src/config/historyFilters.ts +7 -0
- package/src/config/pubsubChannels.ts +7 -27
- package/src/config/roles.ts +12 -10
- package/src/config/workspacePaths.ts +10 -0
- package/src/index.css +1 -1
- package/src/lang/de.ts +39 -220
- package/src/lang/en.ts +39 -191
- package/src/lang/es.ts +39 -218
- package/src/lang/fr.ts +39 -220
- package/src/lang/ja.ts +39 -216
- package/src/lang/ko.ts +39 -216
- package/src/lang/pt-BR.ts +39 -217
- package/src/lang/zh.ts +39 -215
- package/src/main.ts +3 -0
- package/src/plugins/accounting/definition.ts +12 -8
- package/src/plugins/accounting/index.ts +7 -4
- package/src/plugins/accounting/meta.ts +23 -60
- package/src/plugins/manageSkills/View.vue +9 -1
- package/src/plugins/photoLocations/View.vue +2 -2
- package/src/plugins/presentCollection/definition.ts +2 -2
- package/src/plugins/presentCollection/plugin.ts +2 -2
- package/src/plugins/presentCollection/types.ts +2 -2
- package/src/plugins/presentHtml/definition.ts +1 -1
- package/src/plugins/presentMulmoScript/View.vue +2 -2
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/presentSVG/View.vue +2 -2
- package/src/plugins/scheduler/TasksTab.vue +1 -2
- package/src/plugins/skill/View.vue +7 -3
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/plugins/spreadsheet/engine/functions/logical.ts +8 -1
- package/src/plugins/textResponse/View.vue +43 -16
- package/src/plugins/wiki/View.vue +29 -2
- package/src/plugins/wiki/components/WikiGraphView.vue +1 -1
- package/src/plugins/wiki/components/WikiPageBody.vue +4 -1
- package/src/plugins/wiki/helpers.ts +13 -81
- package/src/plugins/wiki/index.ts +1 -1
- package/src/router/guards.ts +1 -1
- package/src/router/index.ts +12 -0
- package/src/router/pageRoutes.ts +2 -0
- package/src/types/dashboard.ts +40 -0
- package/src/types/session.ts +3 -0
- package/src/utils/blobDownload.ts +19 -0
- package/src/utils/collections/presentSeed.ts +1 -1
- package/src/utils/html/customViewSrcdoc.ts +31 -3
- package/src/utils/html/previewCsp.ts +1 -1
- package/src/utils/id.ts +1 -1
- package/src/utils/image/htmlSrcAttrs.ts +1 -1
- package/src/utils/markdown/frontmatter.ts +12 -7
- package/src/utils/markdown/highlight.ts +22 -0
- package/src/utils/markdown/marpCustomSize.ts +2 -2
- package/src/utils/markdown/mermaidExtension.ts +56 -0
- package/src/utils/markdown/mermaidRender.ts +147 -0
- package/src/utils/markdown/setup.ts +17 -4
- package/src/utils/markdown/useMermaid.ts +33 -0
- package/src/utils/session/longRunning.ts +33 -0
- package/src/utils/session/mergeSessions.ts +1 -1
- package/client/assets/JsonEditor-Di5xGeZY.css +0 -1
- package/client/assets/JsonEditor-o5--tPQH.js +0 -10
- package/client/assets/index-tOu5ArRZ.css +0 -2
- package/client/assets/lib-D6Xy0IFc.js +0 -114
- package/client/assets/marp-D6GXA-EB.js +0 -3452
- package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-pU0Mw7Zm.js +0 -1
- package/client/assets/runtime-vue-fFYhnNg3.js +0 -1
- package/client/assets/vue-UDIWDtr8.js +0 -1
- package/client/assets/vue-i18n-CQbxVmNs.js +0 -3
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +0 -4
- package/server/accounting/accountNormalize.ts +0 -32
- package/server/accounting/defaultAccounts.ts +0 -87
- package/server/accounting/eventPublisher.ts +0 -52
- package/server/accounting/journal.ts +0 -252
- package/server/accounting/openingBalances.ts +0 -114
- package/server/accounting/report.ts +0 -237
- package/server/accounting/service.ts +0 -718
- package/server/accounting/snapshotCache.ts +0 -334
- package/server/accounting/timeSeries.ts +0 -265
- package/server/accounting/types.ts +0 -148
- package/server/api/routes/accounting.ts +0 -373
- package/server/api/routes/wiki/frontmatter.ts +0 -34
- package/server/api/routes/wiki/pageIndex.ts +0 -53
- package/server/system/logs/aaa +0 -737
- package/server/system/logs/bb +0 -446
- package/server/utils/files/accounting-io.ts +0 -294
- package/server/workspace/feeds/engine.ts +0 -143
- package/server/workspace/feeds/fetch/httpClient.ts +0 -127
- package/server/workspace/feeds/fetch/rssParser.ts +0 -117
- package/server/workspace/feeds/index.ts +0 -8
- package/server/workspace/feeds/ingestTypes.ts +0 -62
- package/server/workspace/feeds/pathResolver.ts +0 -74
- package/server/workspace/feeds/paths.ts +0 -30
- package/server/workspace/feeds/projectItem.ts +0 -92
- package/server/workspace/feeds/registry.ts +0 -43
- package/server/workspace/feeds/retrievers/httpJson.ts +0 -19
- package/server/workspace/feeds/retrievers/index.ts +0 -27
- package/server/workspace/feeds/retrievers/registerAll.ts +0 -5
- package/server/workspace/feeds/retrievers/rss.ts +0 -24
- package/server/workspace/feeds/state.ts +0 -55
- package/src/composables/useAccountingChannel.ts +0 -58
- package/src/lib/wiki-page/graph.ts +0 -108
- package/src/lib/wiki-page/index-parse.ts +0 -221
- package/src/lib/wiki-page/link.ts +0 -62
- package/src/lib/wiki-page/lint.ts +0 -105
- package/src/lib/wiki-page/paths.ts +0 -35
- package/src/lib/wiki-page/slug.ts +0 -54
- package/src/plugins/accounting/Preview.vue +0 -103
- package/src/plugins/accounting/View.vue +0 -633
- package/src/plugins/accounting/actions.ts +0 -34
- package/src/plugins/accounting/api.ts +0 -301
- package/src/plugins/accounting/components/AccountEditor.vue +0 -250
- package/src/plugins/accounting/components/AccountRow.vue +0 -50
- package/src/plugins/accounting/components/AccountsList.vue +0 -102
- package/src/plugins/accounting/components/AccountsModal.vue +0 -301
- package/src/plugins/accounting/components/BalanceSheet.vue +0 -186
- package/src/plugins/accounting/components/BookSettings.vue +0 -284
- package/src/plugins/accounting/components/BookSwitcher.vue +0 -78
- package/src/plugins/accounting/components/DateRangePicker.vue +0 -140
- package/src/plugins/accounting/components/JournalEntryForm.vue +0 -505
- package/src/plugins/accounting/components/JournalList.vue +0 -554
- package/src/plugins/accounting/components/Ledger.vue +0 -206
- package/src/plugins/accounting/components/NewBookForm.vue +0 -211
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +0 -272
- package/src/plugins/accounting/components/ProfitLoss.vue +0 -160
- package/src/plugins/accounting/components/accountDraft.ts +0 -13
- package/src/plugins/accounting/components/accountNumbering.ts +0 -103
- package/src/plugins/accounting/components/accountValidation.ts +0 -75
- package/src/plugins/accounting/components/useLatestRequest.ts +0 -44
- package/src/plugins/accounting/countries.ts +0 -158
- package/src/plugins/accounting/currencies.ts +0 -77
- package/src/plugins/accounting/dates.ts +0 -51
- package/src/plugins/accounting/fiscalYear.ts +0 -136
- package/src/plugins/accounting/timeSeriesEnums.ts +0 -16
- package/src/plugins/wiki/route.ts +0 -137
- /package/client/assets/{_plugin-vue_export-helper-B67ILkmu.js → _plugin-vue_export-helper-BDNMzG2s.js} +0 -0
- /package/client/assets/{purify.es-B27wDFIb-51iYcXuK.js → purify.es-B27wDFIb-Bu4Grnl0.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-z2wCIsir.js → typeof-DBp4T-Ny-B5XbjTb1.js} +0 -0
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
// Single fs gateway for the accounting plugin. Every read / write
|
|
2
|
-
// against `data/accounting/...` lives here so callers don't sprinkle
|
|
3
|
-
// raw `fs` / path concatenation across the codebase (CLAUDE.md rule:
|
|
4
|
-
// raw `fs.readFile` / `fs.writeFile` is forbidden in route handlers).
|
|
5
|
-
//
|
|
6
|
-
// Snapshot cache rule: snapshots are derived state. Any write that
|
|
7
|
-
// touches past data must call `invalidateSnapshotsFrom(...)` to drop
|
|
8
|
-
// stale snapshot files; the next read regenerates lazily via
|
|
9
|
-
// `server/accounting/snapshotCache.ts`. The journal JSONL files are
|
|
10
|
-
// the single source of truth.
|
|
11
|
-
|
|
12
|
-
import { promises as fsPromises } from "node:fs";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
|
|
15
|
-
import { workspacePath, WORKSPACE_DIRS } from "../../workspace/paths.js";
|
|
16
|
-
import { writeJsonAtomic } from "./json.js";
|
|
17
|
-
import { isEnoent } from "./safe.js";
|
|
18
|
-
import type { AccountingConfig, Account, JournalEntry, MonthSnapshot } from "../../accounting/types.js";
|
|
19
|
-
|
|
20
|
-
const root = (workspaceRoot?: string): string => workspaceRoot ?? workspacePath;
|
|
21
|
-
|
|
22
|
-
function accountingRoot(workspaceRoot?: string): string {
|
|
23
|
-
return path.join(root(workspaceRoot), WORKSPACE_DIRS.accounting);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function configPath(workspaceRoot?: string): string {
|
|
27
|
-
return path.join(accountingRoot(workspaceRoot), "config.json");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Allowed shape for a book id used as a directory name. Defense
|
|
31
|
-
* against path traversal: a crafted id like "../../config" or
|
|
32
|
-
* "/tmp/x" would otherwise let `bookRoot` escape the
|
|
33
|
-
* `data/accounting/books/` tree, since every write path joins
|
|
34
|
-
* `bookId` directly into the filesystem. The first character is
|
|
35
|
-
* alphanumeric to forbid leading dashes / underscores that some
|
|
36
|
-
* shells / docs render confusingly; `_` and `-` are allowed inside.
|
|
37
|
-
* 64 chars is plenty for any reasonable book name. */
|
|
38
|
-
const SAFE_BOOK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
39
|
-
|
|
40
|
-
export function isSafeBookId(bookId: string): boolean {
|
|
41
|
-
return typeof bookId === "string" && SAFE_BOOK_ID_RE.test(bookId);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function assertSafeBookId(bookId: string): void {
|
|
45
|
-
if (!isSafeBookId(bookId)) {
|
|
46
|
-
throw new Error(`accounting: invalid bookId ${JSON.stringify(bookId)} (allowed: alphanumeric / _ / -; 1-64 chars; cannot start with _ or -)`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function bookRoot(bookId: string, workspaceRoot?: string): string {
|
|
51
|
-
assertSafeBookId(bookId);
|
|
52
|
-
return path.join(root(workspaceRoot), WORKSPACE_DIRS.accountingBooks, bookId);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function accountsPath(bookId: string, workspaceRoot?: string): string {
|
|
56
|
-
return path.join(bookRoot(bookId, workspaceRoot), "accounts.json");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function journalDir(bookId: string, workspaceRoot?: string): string {
|
|
60
|
-
return path.join(bookRoot(bookId, workspaceRoot), "journal");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function journalFileFor(bookId: string, period: string, workspaceRoot?: string): string {
|
|
64
|
-
return path.join(journalDir(bookId, workspaceRoot), `${period}.jsonl`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function snapshotsDir(bookId: string, workspaceRoot?: string): string {
|
|
68
|
-
return path.join(bookRoot(bookId, workspaceRoot), "snapshots");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function snapshotFileFor(bookId: string, period: string, workspaceRoot?: string): string {
|
|
72
|
-
return path.join(snapshotsDir(bookId, workspaceRoot), `${period}.json`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
76
|
-
try {
|
|
77
|
-
await fsPromises.access(filePath);
|
|
78
|
-
return true;
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Strict variant of `readJsonOrNull` from `./json.ts`: returns null
|
|
85
|
-
* on ENOENT but RETHROWS other read errors and parse failures so a
|
|
86
|
-
* corrupted accounting journal surfaces rather than silently
|
|
87
|
-
* collapsing to "no data". `./json.ts` keeps the permissive
|
|
88
|
-
* variant for user-config files where a single bad keystroke
|
|
89
|
-
* shouldn't 500 the server. */
|
|
90
|
-
async function readJsonStrict<T>(filePath: string): Promise<T | null> {
|
|
91
|
-
try {
|
|
92
|
-
const raw = await fsPromises.readFile(filePath, "utf-8");
|
|
93
|
-
return JSON.parse(raw) as T;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
if (isEnoent(err)) return null;
|
|
96
|
-
throw err;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── config.json ────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
export async function readConfig(workspaceRoot?: string): Promise<AccountingConfig | null> {
|
|
103
|
-
return readJsonStrict<AccountingConfig>(configPath(workspaceRoot));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function writeConfig(config: AccountingConfig, workspaceRoot?: string): Promise<void> {
|
|
107
|
-
await writeJsonAtomic(configPath(workspaceRoot), config);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ── accounts.json ──────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
export async function readAccounts(bookId: string, workspaceRoot?: string): Promise<Account[]> {
|
|
113
|
-
const accounts = await readJsonStrict<Account[]>(accountsPath(bookId, workspaceRoot));
|
|
114
|
-
return accounts ?? [];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function writeAccounts(bookId: string, accounts: Account[], workspaceRoot?: string): Promise<void> {
|
|
118
|
-
await writeJsonAtomic(accountsPath(bookId, workspaceRoot), accounts);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── journal/YYYY-MM.jsonl (append-only) ────────────────────────────
|
|
122
|
-
|
|
123
|
-
/** Convert a YYYY-MM-DD date string to its YYYY-MM month bucket. The
|
|
124
|
-
* month bucket dictates which JSONL file the entry lives in. */
|
|
125
|
-
export function periodFromDate(date: string): string {
|
|
126
|
-
// YYYY-MM-DD → YYYY-MM. Validate the prefix shape so a malformed
|
|
127
|
-
// input fails early instead of silently bucketing into "1970-01"
|
|
128
|
-
// or similar.
|
|
129
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
130
|
-
throw new Error(`accounting: invalid date format ${JSON.stringify(date)} (expected YYYY-MM-DD)`);
|
|
131
|
-
}
|
|
132
|
-
return date.slice(0, 7);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Append one entry to the appropriate month's JSONL.
|
|
136
|
-
*
|
|
137
|
-
* Uses POSIX append-only semantics (`fs.appendFile` → `O_APPEND`).
|
|
138
|
-
* Two concurrent callers landing in the same month file are
|
|
139
|
-
* serialised by the kernel — neither overwrites the other, which
|
|
140
|
-
* is the bug the previous read-modify-write implementation had.
|
|
141
|
-
*
|
|
142
|
-
* Crash mid-write: an entry shorter than `PIPE_BUF` (≥ 512 bytes
|
|
143
|
-
* on every supported platform) writes atomically; a single
|
|
144
|
-
* serialised `JournalEntry` is comfortably under that. If the
|
|
145
|
-
* process is killed during the syscall the worst case is a torn
|
|
146
|
-
* trailing line, which `readJournalMonth` already tolerates by
|
|
147
|
-
* skipping unparseable lines and surfacing a `skipped` count to
|
|
148
|
-
* the caller. */
|
|
149
|
-
export async function appendJournal(bookId: string, entry: JournalEntry, workspaceRoot?: string): Promise<void> {
|
|
150
|
-
const period = periodFromDate(entry.date);
|
|
151
|
-
const file = journalFileFor(bookId, period, workspaceRoot);
|
|
152
|
-
await fsPromises.mkdir(path.dirname(file), { recursive: true });
|
|
153
|
-
await fsPromises.appendFile(file, `${JSON.stringify(entry)}\n`, { encoding: "utf-8" });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function groupEntriesByPeriod(entries: readonly JournalEntry[]): Map<string, JournalEntry[]> {
|
|
157
|
-
const byPeriod = new Map<string, JournalEntry[]>();
|
|
158
|
-
for (const entry of entries) {
|
|
159
|
-
const period = periodFromDate(entry.date);
|
|
160
|
-
const list = byPeriod.get(period) ?? [];
|
|
161
|
-
list.push(entry);
|
|
162
|
-
byPeriod.set(period, list);
|
|
163
|
-
}
|
|
164
|
-
return byPeriod;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Append a batch of entries: same-period entries are concatenated
|
|
168
|
-
* into one `appendFile` call so the whole same-period chunk hits
|
|
169
|
-
* the kernel as a single `O_APPEND` write — small chunks (under
|
|
170
|
-
* `PIPE_BUF`, ≥ 512 bytes on every supported platform) are
|
|
171
|
-
* guaranteed atomic by POSIX, and `O_APPEND` serialises with any
|
|
172
|
-
* concurrent appender (a parallel `appendJournal` / `addEntries`
|
|
173
|
-
* call can never overwrite our write or vice versa). Cross-period
|
|
174
|
-
* batches loop one append per period; each is independently
|
|
175
|
-
* concurrency-safe but their union is not transactional across
|
|
176
|
-
* files (out of scope for the append-only JSONL design). */
|
|
177
|
-
export async function appendJournalBatch(bookId: string, entries: readonly JournalEntry[], workspaceRoot?: string): Promise<void> {
|
|
178
|
-
if (entries.length === 0) return;
|
|
179
|
-
const byPeriod = groupEntriesByPeriod(entries);
|
|
180
|
-
for (const [period, items] of byPeriod) {
|
|
181
|
-
const file = journalFileFor(bookId, period, workspaceRoot);
|
|
182
|
-
await fsPromises.mkdir(path.dirname(file), { recursive: true });
|
|
183
|
-
const chunk = items.map((entry) => `${JSON.stringify(entry)}\n`).join("");
|
|
184
|
-
await fsPromises.appendFile(file, chunk, { encoding: "utf-8" });
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Read a single month's JSONL. Malformed lines are skipped (logged
|
|
189
|
-
* by the caller; this layer just returns the parseable subset) so
|
|
190
|
-
* one bad line doesn't lock the user out of their book. */
|
|
191
|
-
export async function readJournalMonth(bookId: string, period: string, workspaceRoot?: string): Promise<{ entries: JournalEntry[]; skipped: number }> {
|
|
192
|
-
const file = journalFileFor(bookId, period, workspaceRoot);
|
|
193
|
-
let raw: string;
|
|
194
|
-
try {
|
|
195
|
-
raw = await fsPromises.readFile(file, "utf-8");
|
|
196
|
-
} catch (err) {
|
|
197
|
-
if (isEnoent(err)) return { entries: [], skipped: 0 };
|
|
198
|
-
throw err;
|
|
199
|
-
}
|
|
200
|
-
const entries: JournalEntry[] = [];
|
|
201
|
-
let skipped = 0;
|
|
202
|
-
for (const line of raw.split("\n")) {
|
|
203
|
-
if (line.trim() === "") continue;
|
|
204
|
-
try {
|
|
205
|
-
entries.push(JSON.parse(line) as JournalEntry);
|
|
206
|
-
} catch {
|
|
207
|
-
skipped += 1;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return { entries, skipped };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** List the YYYY-MM periods that have a journal file on disk, sorted
|
|
214
|
-
* ascending. Useful for full-history scans (rebuilding snapshots
|
|
215
|
-
* from scratch). */
|
|
216
|
-
export async function listJournalPeriods(bookId: string, workspaceRoot?: string): Promise<string[]> {
|
|
217
|
-
let names: string[];
|
|
218
|
-
try {
|
|
219
|
-
names = await fsPromises.readdir(journalDir(bookId, workspaceRoot));
|
|
220
|
-
} catch (err) {
|
|
221
|
-
if (isEnoent(err)) return [];
|
|
222
|
-
throw err;
|
|
223
|
-
}
|
|
224
|
-
return names
|
|
225
|
-
.filter((name) => /^\d{4}-\d{2}\.jsonl$/.test(name))
|
|
226
|
-
.map((name) => name.slice(0, 7))
|
|
227
|
-
.sort();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── snapshots/YYYY-MM.json (cache, not source of truth) ────────────
|
|
231
|
-
|
|
232
|
-
export async function readSnapshot(bookId: string, period: string, workspaceRoot?: string): Promise<MonthSnapshot | null> {
|
|
233
|
-
return readJsonStrict<MonthSnapshot>(snapshotFileFor(bookId, period, workspaceRoot));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export async function writeSnapshot(bookId: string, snapshot: MonthSnapshot, workspaceRoot?: string): Promise<void> {
|
|
237
|
-
const file = snapshotFileFor(bookId, snapshot.period, workspaceRoot);
|
|
238
|
-
await fsPromises.mkdir(path.dirname(file), { recursive: true });
|
|
239
|
-
// `uniqueTmp` guards against the lazy fallback in `getOrBuildSnapshot`
|
|
240
|
-
// racing with the background rebuild — both can land in
|
|
241
|
-
// `writeSnapshot` for the same period at once. Distinct tmp file
|
|
242
|
-
// names mean each writer renames its own; the destination
|
|
243
|
-
// overwrites idempotently (both derive from the same journal).
|
|
244
|
-
await writeJsonAtomic(file, snapshot, { uniqueTmp: true });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Drop snapshot files for all periods >= `fromPeriod`. The next
|
|
248
|
-
* read regenerates them. Idempotent: missing files are silently
|
|
249
|
-
* ignored. */
|
|
250
|
-
export async function invalidateSnapshotsFrom(bookId: string, fromPeriod: string, workspaceRoot?: string): Promise<{ removed: string[] }> {
|
|
251
|
-
let names: string[];
|
|
252
|
-
try {
|
|
253
|
-
names = await fsPromises.readdir(snapshotsDir(bookId, workspaceRoot));
|
|
254
|
-
} catch (err) {
|
|
255
|
-
if (isEnoent(err)) return { removed: [] };
|
|
256
|
-
throw err;
|
|
257
|
-
}
|
|
258
|
-
const removed: string[] = [];
|
|
259
|
-
for (const name of names) {
|
|
260
|
-
const match = /^(\d{4}-\d{2})\.json$/.exec(name);
|
|
261
|
-
if (!match) continue;
|
|
262
|
-
const [, period] = match;
|
|
263
|
-
if (period >= fromPeriod) {
|
|
264
|
-
await fsPromises.rm(path.join(snapshotsDir(bookId, workspaceRoot), name), { force: true });
|
|
265
|
-
removed.push(period);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return { removed: removed.sort() };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Drop ALL snapshots for a book — used by `rebuildSnapshots()`
|
|
272
|
-
* with no `from`. Equivalent to `invalidateSnapshotsFrom("0000-00")`
|
|
273
|
-
* but reads more clearly at call sites. */
|
|
274
|
-
export async function invalidateAllSnapshots(bookId: string, workspaceRoot?: string): Promise<{ removed: string[] }> {
|
|
275
|
-
return invalidateSnapshotsFrom(bookId, "0000-00", workspaceRoot);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── book directory housekeeping ────────────────────────────────────
|
|
279
|
-
|
|
280
|
-
export async function bookExists(bookId: string, workspaceRoot?: string): Promise<boolean> {
|
|
281
|
-
return fileExists(bookRoot(bookId, workspaceRoot));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export async function ensureBookDir(bookId: string, workspaceRoot?: string): Promise<void> {
|
|
285
|
-
await fsPromises.mkdir(bookRoot(bookId, workspaceRoot), { recursive: true });
|
|
286
|
-
await fsPromises.mkdir(journalDir(bookId, workspaceRoot), { recursive: true });
|
|
287
|
-
await fsPromises.mkdir(snapshotsDir(bookId, workspaceRoot), { recursive: true });
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Recursively delete a book's directory. Used by `deleteBook` after
|
|
291
|
-
* the config has been updated to drop the entry. */
|
|
292
|
-
export async function removeBookDir(bookId: string, workspaceRoot?: string): Promise<void> {
|
|
293
|
-
await fsPromises.rm(bookRoot(bookId, workspaceRoot), { recursive: true, force: true });
|
|
294
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
// Retrieval engine: fetch a feed, upsert its records into the
|
|
2
|
-
// collection's data dir (keyed by primaryKey, so re-fetches replace in
|
|
3
|
-
// place / accumulate by id), and persist per-feed state. Per-feed
|
|
4
|
-
// failures are isolated — `refreshOne` never throws; `refreshDue`
|
|
5
|
-
// processes feeds sequentially to stay gentle on remote hosts (the
|
|
6
|
-
// fetch client does no rate-limiting yet).
|
|
7
|
-
|
|
8
|
-
import { workspacePath } from "../workspace.js";
|
|
9
|
-
import { log } from "../../system/logger/index.js";
|
|
10
|
-
import { deleteItem, listItems, writeItem, type CollectionItem, type CollectionSchema, type LoadedCollection } from "../collections/index.js";
|
|
11
|
-
import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
|
|
12
|
-
import { getRetriever } from "./retrievers/index.js";
|
|
13
|
-
import "./retrievers/registerAll.js";
|
|
14
|
-
import { listFeeds } from "./registry.js";
|
|
15
|
-
import { readFeedState, writeFeedState, type FeedState } from "./state.js";
|
|
16
|
-
import { DEFAULT_FEED_MAX_ITEMS, type FeedSchedule, type IngestSpec } from "./ingestTypes.js";
|
|
17
|
-
|
|
18
|
-
/** Feed schemas carry the rich `IngestSpec` (validated at discovery —
|
|
19
|
-
* `source === "feed"` requires `ingest`), but the canonical
|
|
20
|
-
* `CollectionSchema.ingest` only promises the minimal `CollectionIngest`.
|
|
21
|
-
* Narrow here so the engine can read the retrieval fields type-safely. */
|
|
22
|
-
function feedIngest(schema: CollectionSchema): IngestSpec | undefined {
|
|
23
|
-
return schema.ingest as IngestSpec | undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RefreshResult {
|
|
27
|
-
slug: string;
|
|
28
|
-
written: number;
|
|
29
|
-
/** Old records deleted by the maxItems cap this run. */
|
|
30
|
-
removed: number;
|
|
31
|
-
errors: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function upsertItems(workspaceRoot: string, feed: LoadedCollection, items: CollectionItem[]): Promise<number> {
|
|
35
|
-
let written = 0;
|
|
36
|
-
for (const item of items) {
|
|
37
|
-
const itemId = item[feed.schema.primaryKey];
|
|
38
|
-
if (typeof itemId !== "string" || itemId.length === 0) continue;
|
|
39
|
-
const result = await writeItem(feed.dataDir, itemId, item, { refuseOverwrite: false, workspaceRoot, slug: feed.slug });
|
|
40
|
-
if (result.kind === "ok") written += 1;
|
|
41
|
-
else log.warn("feeds", "feed item write skipped", { slug: feed.slug, itemId, kind: result.kind });
|
|
42
|
-
}
|
|
43
|
-
return written;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The schema's first `date` field, used to order records for the
|
|
47
|
-
* maxItems cap. Null when the schema declares none. */
|
|
48
|
-
function firstDateField(schema: CollectionSchema): string | null {
|
|
49
|
-
for (const [key, spec] of Object.entries(schema.fields)) {
|
|
50
|
-
if (spec.type === "date") return key;
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Epoch ms for a record's date value; missing / unparseable sorts oldest.
|
|
56
|
-
function recordTime(item: CollectionItem, field: string): number {
|
|
57
|
-
const value = item[field];
|
|
58
|
-
const millis = typeof value === "string" ? Date.parse(value) : NaN;
|
|
59
|
-
return Number.isFinite(millis) ? millis : Number.NEGATIVE_INFINITY;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Enforce `ingest.maxItems` (default 100): keep the newest N records by
|
|
63
|
-
* the schema's date field, delete the rest. No-op when the cap is 0/absent
|
|
64
|
-
* of a date field, or when under the cap. Returns the number deleted. */
|
|
65
|
-
async function pruneFeed(workspaceRoot: string, feed: LoadedCollection): Promise<number> {
|
|
66
|
-
const cap = feedIngest(feed.schema)?.maxItems ?? DEFAULT_FEED_MAX_ITEMS;
|
|
67
|
-
if (cap <= 0) return 0;
|
|
68
|
-
const dateField = firstDateField(feed.schema);
|
|
69
|
-
if (!dateField) {
|
|
70
|
-
log.warn("feeds", "maxItems prune skipped: schema has no date field to order by", { slug: feed.slug });
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
73
|
-
const items = await listItems(feed.dataDir, { workspaceRoot });
|
|
74
|
-
if (items.length <= cap) return 0;
|
|
75
|
-
const stale = [...items].sort((left, right) => recordTime(right, dateField) - recordTime(left, dateField)).slice(cap);
|
|
76
|
-
let removed = 0;
|
|
77
|
-
for (const item of stale) {
|
|
78
|
-
const itemId = item[feed.schema.primaryKey];
|
|
79
|
-
if (typeof itemId !== "string" || itemId.length === 0) continue;
|
|
80
|
-
if ((await deleteItem(feed.dataDir, itemId, { workspaceRoot, slug: feed.slug })).kind === "ok") removed += 1;
|
|
81
|
-
}
|
|
82
|
-
if (removed > 0) log.info("feeds", "pruned old feed records", { slug: feed.slug, removed, cap });
|
|
83
|
-
return removed;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Fetch one feed now, upsert its records, then enforce the maxItems cap.
|
|
87
|
-
* Failure-isolated: returns an errors array rather than throwing. */
|
|
88
|
-
export async function refreshOne(workspaceRoot: string, feed: LoadedCollection): Promise<RefreshResult> {
|
|
89
|
-
const { slug } = feed;
|
|
90
|
-
const ingest = feedIngest(feed.schema);
|
|
91
|
-
if (!ingest) return { slug, written: 0, removed: 0, errors: ["collection has no ingest config"] };
|
|
92
|
-
const retriever = getRetriever(ingest.kind);
|
|
93
|
-
if (!retriever) return { slug, written: 0, removed: 0, errors: [`no retriever registered for kind '${ingest.kind}'`] };
|
|
94
|
-
const state = await readFeedState(workspaceRoot, slug);
|
|
95
|
-
try {
|
|
96
|
-
const result = await retriever(ingest, feed.schema, state);
|
|
97
|
-
const written = await upsertItems(workspaceRoot, feed, result.items);
|
|
98
|
-
await writeFeedState(workspaceRoot, slug, { ...state, lastFetchedAt: new Date().toISOString(), cursor: result.cursor, consecutiveFailures: 0 });
|
|
99
|
-
const removed = await pruneFeed(workspaceRoot, feed);
|
|
100
|
-
log.info("feeds", "feed refreshed", { slug, written, removed, fetched: result.items.length });
|
|
101
|
-
return { slug, written, removed, errors: [] };
|
|
102
|
-
} catch (error) {
|
|
103
|
-
await writeFeedState(workspaceRoot, slug, { ...state, consecutiveFailures: state.consecutiveFailures + 1 });
|
|
104
|
-
const message = String(error);
|
|
105
|
-
log.warn("feeds", "feed refresh failed", { slug, error: message });
|
|
106
|
-
return { slug, written: 0, removed: 0, errors: [message] };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function dueIntervalMs(schedule: FeedSchedule): number {
|
|
111
|
-
switch (schedule) {
|
|
112
|
-
case "daily":
|
|
113
|
-
return ONE_DAY_MS;
|
|
114
|
-
case "weekly":
|
|
115
|
-
return 7 * ONE_DAY_MS;
|
|
116
|
-
default:
|
|
117
|
-
return ONE_HOUR_MS;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** True iff a feed is due to refresh given its schedule + last fetch.
|
|
122
|
-
* `on-demand` feeds are never auto-due. */
|
|
123
|
-
function isFeedDue(feed: LoadedCollection, state: FeedState): boolean {
|
|
124
|
-
const schedule = feedIngest(feed.schema)?.schedule;
|
|
125
|
-
if (!schedule || schedule === "on-demand") return false;
|
|
126
|
-
if (!state.lastFetchedAt) return true;
|
|
127
|
-
const elapsed = Date.now() - Date.parse(state.lastFetchedAt);
|
|
128
|
-
if (!Number.isFinite(elapsed)) return true;
|
|
129
|
-
return elapsed >= dueIntervalMs(schedule);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Refresh every feed whose schedule says it's due. Called by the
|
|
133
|
-
* hourly system task. Sequential + failure-isolated. */
|
|
134
|
-
export async function refreshDue(workspaceRoot: string = workspacePath): Promise<RefreshResult[]> {
|
|
135
|
-
const feeds = await listFeeds(workspaceRoot);
|
|
136
|
-
const results: RefreshResult[] = [];
|
|
137
|
-
for (const feed of feeds) {
|
|
138
|
-
const state = await readFeedState(workspaceRoot, feed.slug);
|
|
139
|
-
if (!isFeedDue(feed, state)) continue;
|
|
140
|
-
results.push(await refreshOne(workspaceRoot, feed));
|
|
141
|
-
}
|
|
142
|
-
return results;
|
|
143
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// Minimal HTTP client for feed retrievers. Feed URLs are model-authored /
|
|
2
|
-
// user-supplied, so beyond a User-Agent + timeout this guards against SSRF:
|
|
3
|
-
// every URL (and every redirect hop) is DNS-resolved and rejected if it
|
|
4
|
-
// points at a loopback / private / link-local / cloud-metadata address.
|
|
5
|
-
// Redirects are followed MANUALLY so a public URL can't 302 to an internal
|
|
6
|
-
// one and bypass the guard. It does NOT do robots.txt / rate limiting (the
|
|
7
|
-
// engine fetches feeds sequentially to stay gentle).
|
|
8
|
-
|
|
9
|
-
import { lookup } from "node:dns/promises";
|
|
10
|
-
import { isIP } from "node:net";
|
|
11
|
-
import { ONE_SECOND_MS } from "../../../utils/time.js";
|
|
12
|
-
|
|
13
|
-
/** Identifies the bot to site operators. */
|
|
14
|
-
export const FEED_USER_AGENT = "MulmoClaude-FeedBot/1.0 (+https://github.com/receptron/mulmoclaude)";
|
|
15
|
-
|
|
16
|
-
/** Per-request wall-clock cap so a hung server can't wedge a refresh. */
|
|
17
|
-
export const DEFAULT_FEED_TIMEOUT_MS = 30 * ONE_SECOND_MS;
|
|
18
|
-
|
|
19
|
-
/** Cap on redirect hops followed (each re-checked for SSRF). */
|
|
20
|
-
const MAX_REDIRECTS = 5;
|
|
21
|
-
|
|
22
|
-
// CIDR blocks we refuse to fetch: unspecified, loopback, RFC1918, CGNAT,
|
|
23
|
-
// and link-local (which also covers the 169.254.169.254 metadata IP).
|
|
24
|
-
/* eslint-disable sonarjs/no-hardcoded-ip -- intentional SSRF deny-list of loopback / private / link-local / CGNAT CIDRs */
|
|
25
|
-
const BLOCKED_V4_CIDRS: readonly (readonly [string, number])[] = [
|
|
26
|
-
["0.0.0.0", 8],
|
|
27
|
-
["10.0.0.0", 8],
|
|
28
|
-
["100.64.0.0", 10],
|
|
29
|
-
["127.0.0.0", 8],
|
|
30
|
-
["169.254.0.0", 16],
|
|
31
|
-
["172.16.0.0", 12],
|
|
32
|
-
["192.168.0.0", 16],
|
|
33
|
-
];
|
|
34
|
-
/* eslint-enable sonarjs/no-hardcoded-ip */
|
|
35
|
-
|
|
36
|
-
function ipv4ToInt(address: string): number | null {
|
|
37
|
-
const octets = address.split(".").map(Number);
|
|
38
|
-
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return null;
|
|
39
|
-
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isBlockedIpv4(address: string): boolean {
|
|
43
|
-
const value = ipv4ToInt(address);
|
|
44
|
-
if (value === null) return true; // malformed → block
|
|
45
|
-
return BLOCKED_V4_CIDRS.some(([base, bits]) => {
|
|
46
|
-
const baseInt = ipv4ToInt(base) ?? 0;
|
|
47
|
-
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
48
|
-
return (value & mask) === (baseInt & mask);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function isBlockedIpv6(address: string): boolean {
|
|
53
|
-
const lower = address.toLowerCase();
|
|
54
|
-
if (lower === "::1" || lower === "::") return true; // loopback, unspecified
|
|
55
|
-
if (lower.startsWith("fe80")) return true; // link-local fe80::/10
|
|
56
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true; // ULA fc00::/7
|
|
57
|
-
const mapped = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(lower); // IPv4-mapped
|
|
58
|
-
return mapped ? isBlockedIpv4(mapped[1]) : false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** True for any address we must not fetch (also blocks non-IP input). */
|
|
62
|
-
function isBlockedIp(address: string): boolean {
|
|
63
|
-
const kind = isIP(address);
|
|
64
|
-
if (kind === 4) return isBlockedIpv4(address);
|
|
65
|
-
if (kind === 6) return isBlockedIpv6(address);
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Reject non-http(s) URLs and URLs that resolve to a private/loopback
|
|
70
|
-
* address (SSRF guard). Throws with a clear reason; returns void on pass. */
|
|
71
|
-
async function assertFetchableUrl(rawUrl: string): Promise<void> {
|
|
72
|
-
if (!/^https?:\/\//i.test(rawUrl)) throw new Error(`refusing non-http(s) URL: ${rawUrl}`);
|
|
73
|
-
const { hostname } = new URL(rawUrl);
|
|
74
|
-
// URL.hostname keeps the brackets for IPv6 literals (`[::1]`); strip them
|
|
75
|
-
// so isIP / the block check see the bare address.
|
|
76
|
-
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
77
|
-
if (isIP(host)) {
|
|
78
|
-
if (isBlockedIp(host)) throw new Error(`refusing to fetch a private/loopback address: ${host}`);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
let addresses: { address: string }[];
|
|
82
|
-
try {
|
|
83
|
-
addresses = await lookup(host, { all: true });
|
|
84
|
-
} catch (err) {
|
|
85
|
-
throw new Error(`could not resolve host '${host}': ${String(err)}`);
|
|
86
|
-
}
|
|
87
|
-
const blocked = addresses.find((entry) => isBlockedIp(entry.address));
|
|
88
|
-
if (blocked) throw new Error(`refusing to fetch '${host}' — resolves to a private/loopback address (${blocked.address})`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function fetchOnce(url: string, timeoutMs: number): Promise<Response> {
|
|
92
|
-
const controller = new AbortController();
|
|
93
|
-
const timer = setTimeout(() => controller.abort(new DOMException(`feed fetch timed out after ${timeoutMs}ms`, "TimeoutError")), timeoutMs);
|
|
94
|
-
try {
|
|
95
|
-
return await fetch(url, { headers: { "User-Agent": FEED_USER_AGENT }, signal: controller.signal, redirect: "manual" });
|
|
96
|
-
} finally {
|
|
97
|
-
clearTimeout(timer);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Follow redirects manually, re-running the SSRF guard on every hop so a
|
|
102
|
-
// public URL cannot bounce to an internal target.
|
|
103
|
-
async function fetchGuarded(rawUrl: string, timeoutMs: number): Promise<Response> {
|
|
104
|
-
let current = rawUrl;
|
|
105
|
-
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
106
|
-
await assertFetchableUrl(current);
|
|
107
|
-
const response = await fetchOnce(current, timeoutMs);
|
|
108
|
-
const redirect = response.status >= 300 && response.status < 400 && response.status !== 304 ? response.headers.get("location") : null;
|
|
109
|
-
if (!redirect) return response;
|
|
110
|
-
current = new URL(redirect, current).toString();
|
|
111
|
-
}
|
|
112
|
-
throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${rawUrl}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Fetch a URL as text, throwing on guard rejection, network error, or non-2xx. */
|
|
116
|
-
export async function fetchText(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<string> {
|
|
117
|
-
const response = await fetchGuarded(url, timeoutMs);
|
|
118
|
-
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);
|
|
119
|
-
return response.text();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Fetch a URL as parsed JSON, throwing on guard rejection, network error, or non-2xx. */
|
|
123
|
-
export async function fetchJson(url: string, timeoutMs: number = DEFAULT_FEED_TIMEOUT_MS): Promise<unknown> {
|
|
124
|
-
const response = await fetchGuarded(url, timeoutMs);
|
|
125
|
-
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url}`);
|
|
126
|
-
return response.json();
|
|
127
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// RSS 2.0 + Atom 1.0 + RSS 1.0 (RDF) parser for the Feeds engine.
|
|
2
|
-
//
|
|
3
|
-
// Deliberately NON-normalizing: it locates the feed's items and hands
|
|
4
|
-
// each one back as its RAW parsed XML element. The host hard-codes no
|
|
5
|
-
// per-feed field list — `ingest.map` resolves source paths against the
|
|
6
|
-
// raw item (tags are keys; attributes are prefixed `@_`; namespaced tags
|
|
7
|
-
// keep their prefix), and the caller inspects the feed to decide what to
|
|
8
|
-
// map. Generic value handling (text-node unwrapping, date parsing by the
|
|
9
|
-
// target field's declared type) lives in `projectItem.ts`, not here.
|
|
10
|
-
//
|
|
11
|
-
// Own copy of the XML plumbing — the Feeds tree does not import the
|
|
12
|
-
// legacy `sources` tree. Pure; unit-testable with fixture strings.
|
|
13
|
-
|
|
14
|
-
import { XMLParser } from "fast-xml-parser";
|
|
15
|
-
import { isNonEmptyString, isRecord } from "../../../utils/types.js";
|
|
16
|
-
|
|
17
|
-
export interface ParsedFeedItem {
|
|
18
|
-
/** The raw parsed XML <item>/<entry> object, verbatim. */
|
|
19
|
-
raw: Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ParsedFeed {
|
|
23
|
-
kind: "rss" | "atom";
|
|
24
|
-
title: string | null;
|
|
25
|
-
items: ParsedFeedItem[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const xml = new XMLParser({
|
|
29
|
-
ignoreAttributes: false,
|
|
30
|
-
attributeNamePrefix: "@_",
|
|
31
|
-
cdataPropName: "#cdata",
|
|
32
|
-
parseTagValue: false,
|
|
33
|
-
parseAttributeValue: false,
|
|
34
|
-
trimValues: true,
|
|
35
|
-
// Only the item containers are forced to arrays (single vs. many).
|
|
36
|
-
// Everything else stays in its natural parsed shape so `ingest.map`
|
|
37
|
-
// resolves paths the way the caller sees them in the feed.
|
|
38
|
-
isArray: (name) => name === "item" || name === "entry",
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/** Parse an RSS/Atom/RDF feed body. Returns null when the input doesn't
|
|
42
|
-
* look like a feed we understand. */
|
|
43
|
-
export function parseFeed(body: string): ParsedFeed | null {
|
|
44
|
-
const text = stripBom(body);
|
|
45
|
-
if (!text.trim()) return null;
|
|
46
|
-
let parsed: unknown;
|
|
47
|
-
try {
|
|
48
|
-
parsed = xml.parse(text);
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
if (!isRecord(parsed)) return null;
|
|
53
|
-
if (isRecord(parsed.rss)) return parseRss(parsed.rss);
|
|
54
|
-
if (isRecord(parsed.feed)) return parseAtom(parsed.feed);
|
|
55
|
-
const rdf = parsed["rdf:RDF"] ?? parsed.RDF;
|
|
56
|
-
if (isRecord(rdf)) return parseRss10(rdf);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Collect the raw item objects, skipping anything that isn't a record or
|
|
61
|
-
// carries no <title> (drops feed-level noise / empty entries).
|
|
62
|
-
function collectItems(value: unknown): ParsedFeedItem[] {
|
|
63
|
-
const rawItems = Array.isArray(value) ? value : [];
|
|
64
|
-
const items: ParsedFeedItem[] = [];
|
|
65
|
-
for (const raw of rawItems) {
|
|
66
|
-
if (isRecord(raw) && readString(raw.title) !== null) items.push({ raw });
|
|
67
|
-
}
|
|
68
|
-
return items;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseRss(rss: Record<string, unknown>): ParsedFeed | null {
|
|
72
|
-
const { channel } = rss;
|
|
73
|
-
if (!isRecord(channel)) return null;
|
|
74
|
-
return { kind: "rss", title: readString(channel.title), items: collectItems(channel.item) };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function parseRss10(rdf: Record<string, unknown>): ParsedFeed | null {
|
|
78
|
-
const channel = isRecord(rdf.channel) ? rdf.channel : null;
|
|
79
|
-
return { kind: "rss", title: channel ? readString(channel.title) : null, items: collectItems(rdf.item) };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function parseAtom(feed: Record<string, unknown>): ParsedFeed | null {
|
|
83
|
-
return { kind: "atom", title: readString(feed.title), items: collectItems(feed.entry) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// --- helpers ------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
// Extract a string from a plain string, a `{ "#text" }` / `{ "#cdata" }`
|
|
89
|
-
// node, or an array (first non-empty). Used only to read the feed/item
|
|
90
|
-
// title for the channel name + the empty-item filter.
|
|
91
|
-
function readString(value: unknown): string | null {
|
|
92
|
-
if (isNonEmptyString(value)) return value;
|
|
93
|
-
if (typeof value === "string") return null;
|
|
94
|
-
if (isRecord(value)) return readStringFromRecord(value);
|
|
95
|
-
if (Array.isArray(value)) return readStringFromArray(value);
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function readStringFromRecord(record: Record<string, unknown>): string | null {
|
|
100
|
-
const text = record["#text"];
|
|
101
|
-
if (isNonEmptyString(text)) return text;
|
|
102
|
-
const cdata = record["#cdata"];
|
|
103
|
-
if (isNonEmptyString(cdata)) return cdata;
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readStringFromArray(array: readonly unknown[]): string | null {
|
|
108
|
-
for (const entry of array) {
|
|
109
|
-
const resolved = readString(entry);
|
|
110
|
-
if (resolved !== null) return resolved;
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function stripBom(text: string): string {
|
|
116
|
-
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
117
|
-
}
|