excaliagent 0.2.0 → 0.4.0
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 +129 -30
- package/dist/server/args.js +19 -3
- package/dist/server/characters.js +540 -0
- package/dist/server/cli.js +47 -6
- package/dist/server/http.js +122 -11
- package/dist/server/hub.js +17 -3
- package/dist/server/instance.js +52 -0
- package/dist/server/panes/herdr.js +29 -3
- package/dist/server/scene-store.js +58 -7
- package/dist/server/sources/claude.js +36 -1
- package/dist/server/sources/codex-live.js +279 -0
- package/dist/server/sources/codex-rollout.js +154 -0
- package/dist/server/sources/codex.js +363 -0
- package/dist/server/sources/jsonl.js +142 -0
- package/dist/server/sources/multi.js +53 -0
- package/dist/server/sources/opencode-live.js +144 -0
- package/dist/server/sources/opencode.js +265 -0
- package/dist/server/sources/proc.js +34 -0
- package/dist/server/sources/transcript.js +9 -103
- package/dist/server/token.js +23 -0
- package/dist/shared/packs.js +62 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/{abnfDiagram-VCTEODGH-tmjYj3VY.js → abnfDiagram-VCTEODGH-BxrKsDoB.js} +1 -1
- package/dist/web/assets/{ar-SA-G6X2FPQ2-8ZGGBDRT.js → ar-SA-G6X2FPQ2-Cc6gbg88.js} +1 -1
- package/dist/web/assets/{architecture-7GRP2DOG-CsvDkWt3.js → architecture-7GRP2DOG-CIqN5I9B.js} +1 -1
- package/dist/web/assets/{architectureDiagram-5GKGNRK7-Bq-1CDhU.js → architectureDiagram-5GKGNRK7-DsXC8AqK.js} +1 -1
- package/dist/web/assets/{az-AZ-76LH7QW2-DbdS_q-w.js → az-AZ-76LH7QW2-BH8VzNec.js} +1 -1
- package/dist/web/assets/{bg-BG-XCXSNQG7-CANqXhG1.js → bg-BG-XCXSNQG7-ClFUbics.js} +1 -1
- package/dist/web/assets/{blockDiagram-I7D4REHJ-DtYn-xOl.js → blockDiagram-I7D4REHJ-CltS-Zt2.js} +1 -1
- package/dist/web/assets/{bn-BD-2XOGV67Q-DJdq0B1R.js → bn-BD-2XOGV67Q-BVUiOO4B.js} +1 -1
- package/dist/web/assets/{c4Diagram-7LVT6UL2-DW6Pzj1e.js → c4Diagram-7LVT6UL2-C-C_SvWm.js} +1 -1
- package/dist/web/assets/{ca-ES-6MX7JW3Y-B7pUqerV.js → ca-ES-6MX7JW3Y-CEK8mB8J.js} +1 -1
- package/dist/web/assets/channel-Dom8D_0o.js +1 -0
- package/dist/web/assets/{chunk-4HAMMTFA-_AKNu1--.js → chunk-4HAMMTFA-Bfdmb0Hg.js} +1 -1
- package/dist/web/assets/{chunk-75Z2AOVW-BoMl8Mqw.js → chunk-75Z2AOVW-NYwkutzS.js} +1 -1
- package/dist/web/assets/{chunk-DU6HZSFF-DYHa8ENH.js → chunk-DU6HZSFF-SqE0xdu5.js} +1 -1
- package/dist/web/assets/{chunk-EIO257PC--adgQBo2.js → chunk-EIO257PC-DpOeKaLq.js} +1 -1
- package/dist/web/assets/{chunk-F27PBJKO-CvQi01qZ.js → chunk-F27PBJKO-g9bJrSwZ.js} +1 -1
- package/dist/web/assets/{chunk-GMAD6QVW-ojaxPuMG.js → chunk-GMAD6QVW-B1y2ol5w.js} +1 -1
- package/dist/web/assets/{chunk-GVQU2GXP-DYAeD1IL.js → chunk-GVQU2GXP-BUjRNRXo.js} +1 -1
- package/dist/web/assets/{chunk-IMKFNOWR-oVPWqbpV.js → chunk-IMKFNOWR-DyeQep8U.js} +1 -1
- package/dist/web/assets/{chunk-L3NEJ4N5-BelAXiIN.js → chunk-L3NEJ4N5-BcTVhsWp.js} +1 -1
- package/dist/web/assets/{chunk-OSK3NFVY-CATtjN8l.js → chunk-OSK3NFVY-Db7B8jPm.js} +1 -1
- package/dist/web/assets/{chunk-P2QGCYS3-CbKPCENJ.js → chunk-P2QGCYS3-Cx5iBC5z.js} +1 -1
- package/dist/web/assets/{chunk-POPQ4Y6H-BfOCSezn.js → chunk-POPQ4Y6H-CqVRZDA8.js} +1 -1
- package/dist/web/assets/{chunk-PWAF6VOD-C7O-JWJt.js → chunk-PWAF6VOD-Cf2j9yew.js} +1 -1
- package/dist/web/assets/{chunk-SHT3W25Y-BfEpPCTX.js → chunk-SHT3W25Y-0_N13xgE.js} +1 -1
- package/dist/web/assets/{chunk-SVP7TREG-CaeKcCSu.js → chunk-SVP7TREG-DkRJF9jr.js} +1 -1
- package/dist/web/assets/{chunk-TICWLB2K-Bx6AcLB_.js → chunk-TICWLB2K-D6OC50xH.js} +1 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-BMEkgkEJ.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-BMEkgkEJ.js +1 -0
- package/dist/web/assets/{cs-CZ-2BRQDIVT-DF8hZ2mh.js → cs-CZ-2BRQDIVT-DWt1RUsP.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-Dol6Weuo.js → cynefin-OW5HDTMX-Dx0HzRCl.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-CbOJ2lks.js → cynefinDiagram-5FMLGOSQ-C8AMQNhe.js} +1 -1
- package/dist/web/assets/{da-DK-5WZEPLOC-CgPdNz6E.js → da-DK-5WZEPLOC-BPggh0j6.js} +1 -1
- package/dist/web/assets/{dagre-GXQ25YYZ-CcuaBID7.js → dagre-GXQ25YYZ-f9qU6BFq.js} +1 -1
- package/dist/web/assets/{de-DE-XR44H4JA-DaVeYWRc.js → de-DE-XR44H4JA-B1q1pJrQ.js} +1 -1
- package/dist/web/assets/{diagram-S7CK7UJ4-BR2gIsNx.js → diagram-S7CK7UJ4-Bcq77Nl3.js} +1 -1
- package/dist/web/assets/{diagram-UQ7AKVKN-DwSZsV-Z.js → diagram-UQ7AKVKN-DXzlgVTk.js} +1 -1
- package/dist/web/assets/{diagram-VSXAHHWV-BcJpQHbj.js → diagram-VSXAHHWV-C1vz2FvT.js} +1 -1
- package/dist/web/assets/{diagram-VX7I27RA-CvjRTkwu.js → diagram-VX7I27RA-_c4ucp3r.js} +1 -1
- package/dist/web/assets/{diagram-Z3DM3KII-vg0-r_P4.js → diagram-Z3DM3KII-cUg91K-F.js} +1 -1
- package/dist/web/assets/{dist-D-3cz42D.js → dist-DcwmvBdV.js} +4 -4
- package/dist/web/assets/{ebnfDiagram-PWID7BFC-DZCaMmMd.js → ebnfDiagram-PWID7BFC-9iWfohHg.js} +1 -1
- package/dist/web/assets/{el-GR-BZB4AONW-nYncjO-L.js → el-GR-BZB4AONW-CrhwvNDD.js} +1 -1
- package/dist/web/assets/{en-B4ZKOASM-DqIz0rJz.js → en-B4ZKOASM-DCjnkz5L.js} +1 -1
- package/dist/web/assets/{erDiagram-RLTQ6QDP-DgyrjoaZ.js → erDiagram-RLTQ6QDP-A72uVlS3.js} +1 -1
- package/dist/web/assets/{es-ES-U4NZUMDT-e3WeRDLl.js → es-ES-U4NZUMDT-D0sRCoDL.js} +1 -1
- package/dist/web/assets/{eu-ES-A7QVB2H4-CwXv1t13.js → eu-ES-A7QVB2H4-DLG-liPb.js} +1 -1
- package/dist/web/assets/{eventmodeling-NTZA5JFV-DCTWpAwP.js → eventmodeling-NTZA5JFV-CIKZvDAj.js} +1 -1
- package/dist/web/assets/{fa-IR-HGAKTJCU-D-9plwKu.js → fa-IR-HGAKTJCU-BW3pPOVT.js} +1 -1
- package/dist/web/assets/{fi-FI-Z5N7JZ37-BTK1rs8G.js → fi-FI-Z5N7JZ37-D2XaqwPp.js} +1 -1
- package/dist/web/assets/flowDiagram-HODETNUW-DhQSc3KP.js +1 -0
- package/dist/web/assets/{fr-FR-RHASNOE6-CAuUMPtN.js → fr-FR-RHASNOE6-B-BjRA7r.js} +1 -1
- package/dist/web/assets/{ganttDiagram-EL5Y4UJY-bFfu4BMj.js → ganttDiagram-EL5Y4UJY-cgwUfr0G.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DIl4-oqK.js → gitGraph-4MIJSDKK-CeKopPn3.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-ho3FT8XP.js → gitGraphDiagram-WWUBYQGX-BWjAy7ei.js} +1 -1
- package/dist/web/assets/{gl-ES-HMX3MZ6V-DlKPdjtk.js → gl-ES-HMX3MZ6V-DYUAqB0N.js} +1 -1
- package/dist/web/assets/{he-IL-6SHJWFNN-CLyP0zBo.js → he-IL-6SHJWFNN-CXkYqbuu.js} +1 -1
- package/dist/web/assets/{hi-IN-IWLTKZ5I-kvhBHNzH.js → hi-IN-IWLTKZ5I-BwVBsHWG.js} +1 -1
- package/dist/web/assets/{hu-HU-A5ZG7DT2-Dtd-o-RJ.js → hu-HU-A5ZG7DT2-DkmlE7Aa.js} +1 -1
- package/dist/web/assets/{id-ID-SAP4L64H-VZ3IV-8B.js → id-ID-SAP4L64H-CwkytHX_.js} +1 -1
- package/dist/web/assets/{image-GAAHSSAO-DWvjHHd3.js → image-GAAHSSAO-u-hBxkaK.js} +1 -1
- package/dist/web/assets/index-D43I17T9.js +212 -0
- package/dist/web/assets/{index-DtRVLazd.css → index-DxXUknKX.css} +1 -1
- package/dist/web/assets/{info-A6RAGUB7-DN8pwhKt.js → info-A6RAGUB7-DmadBmci.js} +1 -1
- package/dist/web/assets/{infoDiagram-27XIBGKW-i2JHBJKe.js → infoDiagram-27XIBGKW-D93CKwhy.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-5VMMS53U-DLLdu9-W.js → ishikawaDiagram-5VMMS53U-BQ-x5YXp.js} +1 -1
- package/dist/web/assets/{it-IT-JPQ66NNP-6SULWnRA.js → it-IT-JPQ66NNP-Bc-Qa6KI.js} +1 -1
- package/dist/web/assets/{ja-JP-DBVTYXUO-BbgutQYO.js → ja-JP-DBVTYXUO-BPmQCryQ.js} +1 -1
- package/dist/web/assets/{journeyDiagram-3NMN7TZE-DsysKJxX.js → journeyDiagram-3NMN7TZE-CO_LXx1E.js} +1 -1
- package/dist/web/assets/{kaa-6HZHGXH3-Bc6yhZWQ.js → kaa-6HZHGXH3-DuBFVAjv.js} +1 -1
- package/dist/web/assets/{kab-KAB-ZGHBKWFO-BaICxis-.js → kab-KAB-ZGHBKWFO-DhEFzSTh.js} +1 -1
- package/dist/web/assets/{kanban-definition-UXKFOSKX-w3k_6RhT.js → kanban-definition-UXKFOSKX-B72Jztau.js} +1 -1
- package/dist/web/assets/{kk-KZ-P5N5QNE5-BVsfmPoG.js → kk-KZ-P5N5QNE5-CW7PxU2h.js} +1 -1
- package/dist/web/assets/{km-KH-HSX4SM5Z-BS1M1bUx.js → km-KH-HSX4SM5Z-CYy7yySD.js} +1 -1
- package/dist/web/assets/{ko-KR-MTYHY66A-C0x16zQm.js → ko-KR-MTYHY66A-C5O645Qe.js} +1 -1
- package/dist/web/assets/{ku-TR-6OUDTVRD-BWI9z5E8.js → ku-TR-6OUDTVRD-CKcd_iWr.js} +1 -1
- package/dist/web/assets/{line-BRXlgUrO.js → line-Ckd3sAmP.js} +1 -1
- package/dist/web/assets/{lt-LT-XHIRWOB4-CTDq1zFR.js → lt-LT-XHIRWOB4-ya2_xWiy.js} +1 -1
- package/dist/web/assets/{lv-LV-5QDEKY6T-h8b-E15B.js → lv-LV-5QDEKY6T-cYYwyzmz.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Da_XEVmR.js → mermaid-parser.core-Dbqkvrw-.js} +3 -3
- package/dist/web/assets/{mindmap-definition-YA3MSWOX-DUBdgu2g.js → mindmap-definition-YA3MSWOX-B4onxNgk.js} +1 -1
- package/dist/web/assets/{mr-IN-CRQNXWMA-dTdkz007.js → mr-IN-CRQNXWMA-CfaHpVWe.js} +1 -1
- package/dist/web/assets/{my-MM-5M5IBNSE-CmYwTRil.js → my-MM-5M5IBNSE-BfKiTATq.js} +1 -1
- package/dist/web/assets/{nb-NO-T6EIAALU-DLLYcK-s.js → nb-NO-T6EIAALU-Cz3yDFYg.js} +1 -1
- package/dist/web/assets/{nl-NL-IS3SIHDZ-mtv15Mmm.js → nl-NL-IS3SIHDZ-DDX026n5.js} +1 -1
- package/dist/web/assets/{nn-NO-6E72VCQL-CJDTT3kB.js → nn-NO-6E72VCQL-BcokL_SN.js} +1 -1
- package/dist/web/assets/{oc-FR-POXYY2M6-DdHJTqH4.js → oc-FR-POXYY2M6-bNFsGpEp.js} +1 -1
- package/dist/web/assets/{pa-IN-N4M65BXN-DxzzhbLu.js → pa-IN-N4M65BXN-iIHN9Z8W.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-CzX42SOE.js → packet-AYTQ26CC-BzBpP9hg.js} +1 -1
- package/dist/web/assets/{pegDiagram-XKGWAZYB-1R2Hd3Th.js → pegDiagram-XKGWAZYB-BPScRPVZ.js} +1 -1
- package/dist/web/assets/percentages-BXMCSKIN-EDfR-TFr.js +1 -0
- package/dist/web/assets/{pie-WAS4IAKB-DEI2uslz.js → pie-WAS4IAKB-N-9RHtKL.js} +1 -1
- package/dist/web/assets/{pieDiagram-E7YTZNPT-DOQnFSrs.js → pieDiagram-E7YTZNPT-Bpd_yCG-.js} +1 -1
- package/dist/web/assets/{pl-PL-T2D74RX3-Dpyab0lI.js → pl-PL-T2D74RX3-DeqOx-a4.js} +1 -1
- package/dist/web/assets/{pt-BR-5N22H2LF-DOGrUA6Y.js → pt-BR-5N22H2LF-DG1PCIkb.js} +1 -1
- package/dist/web/assets/{pt-PT-UZXXM6DQ-DAoNiiOg.js → pt-PT-UZXXM6DQ-BsUJk2fq.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AXDQQJYC-CQE1OxRJ.js → quadrantDiagram-AXDQQJYC-CSLCp0xS.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-B-er2ORT.js → radar-RG4KPBEZ-rKULJroO.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-D1Veuu81.js → railroad-74A4TZTK-hDn0WPg3.js} +1 -1
- package/dist/web/assets/{railroad-abnf-HS5TGJTU-DTxLo5az.js → railroad-abnf-HS5TGJTU-DfI0hiCR.js} +1 -1
- package/dist/web/assets/{railroad-ebnf-LZEXJU2U-BmP2IZGe.js → railroad-ebnf-LZEXJU2U-paFQAnqR.js} +1 -1
- package/dist/web/assets/{railroad-peg-WCYAUIDC-Do60gMTC.js → railroad-peg-WCYAUIDC-B-2Um3QI.js} +1 -1
- package/dist/web/assets/{railroadDiagram-O6MQD6OU-5I8Qk1dI.js → railroadDiagram-O6MQD6OU-WkZPN5MA.js} +1 -1
- package/dist/web/assets/{requirementDiagram-BXWQKSXE-CIIM8YmC.js → requirementDiagram-BXWQKSXE-BJULFhuR.js} +1 -1
- package/dist/web/assets/{ro-RO-JPDTUUEW-DR9yu2MV.js → ro-RO-JPDTUUEW-yKjTMizq.js} +1 -1
- package/dist/web/assets/{ru-RU-B4JR7IUQ-B9uIhZUd.js → ru-RU-B4JR7IUQ-DITGdMQL.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BdGmiiCQ.js → sankeyDiagram-P5KCCOFB-b_vfju3s.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-BeCmgCpG.js → sequenceDiagram-WJ2MYXX4-D5n2aEN6.js} +1 -1
- package/dist/web/assets/{si-LK-N5RQ5JYF-C7DYCsOB.js → si-LK-N5RQ5JYF-DaFciStR.js} +1 -1
- package/dist/web/assets/{sk-SK-C5VTKIMK-BbBBLLCs.js → sk-SK-C5VTKIMK-CpyCIFtD.js} +1 -1
- package/dist/web/assets/{sl-SI-NN7IZMDC-BIaW2Iuj.js → sl-SI-NN7IZMDC-cnwvs7FN.js} +1 -1
- package/dist/web/assets/{stateDiagram-D77RDMKH-CmENpSfH.js → stateDiagram-D77RDMKH-CbFjpKDm.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-MOkTt8Mv.js +1 -0
- package/dist/web/assets/subset-shared.chunk-CWAaXrHb.js +1 -0
- package/dist/web/assets/{subset-worker.chunk-Bi16Jh4x.js → subset-worker.chunk-Cebu2JDe.js} +1 -1
- package/dist/web/assets/{sv-SE-XGPEYMSR-D5A1U3nk.js → sv-SE-XGPEYMSR-dOntI1Fk.js} +1 -1
- package/dist/web/assets/{swimlanes-42K2YHIH-_ymwQg8o.js → swimlanes-42K2YHIH-DtmsuzRR.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-v2ue4raa.js +8 -0
- package/dist/web/assets/{ta-IN-2NMHFXQM-BDzXUwiE.js → ta-IN-2NMHFXQM-Comju6i2.js} +1 -1
- package/dist/web/assets/{th-TH-HPSO5L25-DdVnWuWW.js → th-TH-HPSO5L25-uPCBvbue.js} +1 -1
- package/dist/web/assets/{timeline-definition-24CTP7MA-CRcZde4k.js → timeline-definition-24CTP7MA-DKZOmZFD.js} +1 -1
- package/dist/web/assets/{tr-TR-DEFEU3FU-BQwfGN3o.js → tr-TR-DEFEU3FU-LaP2C4go.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BHZZuSXr.js → treeView-Q6P3EWNA-DJii9IFr.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DAzotEQZ.js → treemap-WGGIJYW6-U42pUP2Q.js} +1 -1
- package/dist/web/assets/{uk-UA-QMV73CPH-CFYnWqHD.js → uk-UA-QMV73CPH-u-WBulor.js} +1 -1
- package/dist/web/assets/{vennDiagram-4TSXK5OY-BsNz9Qnn.js → vennDiagram-4TSXK5OY-C8_zJPdp.js} +1 -1
- package/dist/web/assets/{vi-VN-M7AON7JQ-DO3BySmm.js → vi-VN-M7AON7JQ-DOSv3aq5.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-Cc2r8Vp8.js → wardley-WFR3VGLG-DU89dbbh.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VM6X3IG4-47UvJO4N.js → wardleyDiagram-VM6X3IG4-BGnASelu.js} +1 -1
- package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CVl63zr-.js → xychartDiagram-S5SC5T6Z-js8-abv7.js} +1 -1
- package/dist/web/assets/{zh-CN-LNUGB5OW-DKJ2CF-O.js → zh-CN-LNUGB5OW-B8soJHf3.js} +1 -1
- package/dist/web/assets/{zh-HK-E62DVLB3-DHW8OH5j.js → zh-HK-E62DVLB3-DKByBmSJ.js} +1 -1
- package/dist/web/assets/{zh-TW-RAJ6MFWO-B9cMDifZ.js → zh-TW-RAJ6MFWO-CYBStGL6.js} +1 -1
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.svg +12 -0
- package/dist/web/index.html +6 -3
- package/package.json +5 -3
- package/dist/web/assets/channel-gFr9jEgP.js +0 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-9J2CNLBI.js +0 -1
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-9J2CNLBI.js +0 -1
- package/dist/web/assets/flowDiagram-HODETNUW--2QUGuZ7.js +0 -1
- package/dist/web/assets/index-B2hGCC2V.js +0 -212
- package/dist/web/assets/percentages-BXMCSKIN-Br7824d3.js +0 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-BWgn52cG.js +0 -1
- package/dist/web/assets/subset-shared.chunk-d2GNuFHV.js +0 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-fzrjtpfS.js +0 -8
package/dist/server/http.js
CHANGED
|
@@ -3,7 +3,12 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { extname, join, normalize, sep } from "node:path";
|
|
4
4
|
import { timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { SESSION_ID } from "./hub.js";
|
|
6
|
-
|
|
6
|
+
import { validFile } from "./scene-store.js";
|
|
7
|
+
import { StatusError } from "./characters.js";
|
|
8
|
+
/** Cookies ignore the port, so the name carries it: two boards never clobber each other. */
|
|
9
|
+
export const cookieName = (port) => `excaliagent_token_${port}`;
|
|
10
|
+
/** The token is stable per state dir (token.ts), so the cookie may outlive a restart. */
|
|
11
|
+
const COOKIE_MAX_AGE = 365 * 24 * 3600;
|
|
7
12
|
const MAX_BODY = 50 * 1024 * 1024;
|
|
8
13
|
const TYPES = {
|
|
9
14
|
".html": "text/html; charset=utf-8",
|
|
@@ -33,10 +38,10 @@ function sameToken(a, b) {
|
|
|
33
38
|
const y = Buffer.from(b);
|
|
34
39
|
return x.length === y.length && timingSafeEqual(x, y);
|
|
35
40
|
}
|
|
36
|
-
function cookieToken(req) {
|
|
41
|
+
function cookieToken(req, port) {
|
|
37
42
|
for (const part of (req.headers.cookie ?? "").split(";")) {
|
|
38
43
|
const [k, ...v] = part.trim().split("=");
|
|
39
|
-
if (k ===
|
|
44
|
+
if (k === cookieName(port))
|
|
40
45
|
return v.join("=");
|
|
41
46
|
}
|
|
42
47
|
return undefined;
|
|
@@ -49,15 +54,21 @@ function text(res, status, body) {
|
|
|
49
54
|
res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
50
55
|
res.end(body);
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
/** Body limits of the character and pack routes: an image upload, everything else. */
|
|
58
|
+
export const IMAGE_BODY_LIMIT = 1536 * 1024;
|
|
59
|
+
export const SMALL_BODY_LIMIT = 64 * 1024;
|
|
60
|
+
async function readJson(req, limit = MAX_BODY) {
|
|
53
61
|
if (!(req.headers["content-type"] ?? "").startsWith("application/json")) {
|
|
54
62
|
throw new HttpError(415, "expected application/json");
|
|
55
63
|
}
|
|
64
|
+
// Refuse a declared oversize body before reading any of it.
|
|
65
|
+
if (Number(req.headers["content-length"] ?? 0) > limit)
|
|
66
|
+
throw new HttpError(413, "body too large");
|
|
56
67
|
const chunks = [];
|
|
57
68
|
let size = 0;
|
|
58
69
|
for await (const chunk of req) {
|
|
59
70
|
size += chunk.length;
|
|
60
|
-
if (size >
|
|
71
|
+
if (size > limit)
|
|
61
72
|
throw new HttpError(413, "body too large");
|
|
62
73
|
chunks.push(chunk);
|
|
63
74
|
}
|
|
@@ -118,9 +129,77 @@ export function createAppServer(opts) {
|
|
|
118
129
|
unsubscribe();
|
|
119
130
|
});
|
|
120
131
|
}
|
|
121
|
-
|
|
132
|
+
/** GET /api/packs/<pack>/<file>: a custom pack's image, only if that pack lists it. */
|
|
133
|
+
async function packImage(res, rest, version) {
|
|
134
|
+
const parts = rest.split("/");
|
|
135
|
+
if (parts.length !== 2)
|
|
136
|
+
return json(res, 404, { error: "not found" });
|
|
137
|
+
let pack, file;
|
|
138
|
+
try {
|
|
139
|
+
[pack, file] = parts.map((p) => decodeURIComponent(p));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
throw new HttpError(400, "bad path");
|
|
143
|
+
}
|
|
144
|
+
const img = await opts.characters.readImage(pack, file);
|
|
145
|
+
if (!img)
|
|
146
|
+
return json(res, 404, { error: "not found" });
|
|
147
|
+
res.writeHead(200, {
|
|
148
|
+
"content-type": img.mimeType,
|
|
149
|
+
// `?v=<content hash>` (from the pack listing) names these exact bytes.
|
|
150
|
+
"cache-control": version === img.hash ? "private, max-age=31536000, immutable" : "no-store",
|
|
151
|
+
"x-content-type-options": "nosniff",
|
|
152
|
+
// An SVG opened directly must not run scripts on our origin.
|
|
153
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
|
|
154
|
+
});
|
|
155
|
+
res.end(img.bytes);
|
|
156
|
+
}
|
|
157
|
+
async function api(req, res, pathname, search) {
|
|
122
158
|
const route = `${req.method} ${pathname}`;
|
|
159
|
+
if (req.method === "GET" && pathname.startsWith("/api/packs/"))
|
|
160
|
+
return packImage(res, pathname.slice("/api/packs/".length), search.get("v"));
|
|
161
|
+
const chars = opts.characters;
|
|
162
|
+
const body = async () => {
|
|
163
|
+
const b = await readJson(req, pathname === "/api/packs/image" ? IMAGE_BODY_LIMIT : SMALL_BODY_LIMIT);
|
|
164
|
+
if (!b || typeof b !== "object")
|
|
165
|
+
throw new HttpError(400, "expected a JSON object");
|
|
166
|
+
return b;
|
|
167
|
+
};
|
|
123
168
|
switch (route) {
|
|
169
|
+
case "GET /api/characters":
|
|
170
|
+
return json(res, 200, await chars.load());
|
|
171
|
+
case "POST /api/characters/assign": {
|
|
172
|
+
const b = await body();
|
|
173
|
+
return json(res, 200, await chars.assign(b.pack, b.assign));
|
|
174
|
+
}
|
|
175
|
+
case "POST /api/characters/override": {
|
|
176
|
+
const b = await body();
|
|
177
|
+
return json(res, 200, await chars.override(b.sessionId, (b.character ?? null)));
|
|
178
|
+
}
|
|
179
|
+
case "POST /api/characters/active":
|
|
180
|
+
return json(res, 200, await chars.setActive((await body()).pack));
|
|
181
|
+
case "POST /api/packs": {
|
|
182
|
+
const b = await body();
|
|
183
|
+
return json(res, 200, await chars.createPack(b.name, (b.from ?? undefined)));
|
|
184
|
+
}
|
|
185
|
+
case "POST /api/packs/rename": {
|
|
186
|
+
const b = await body();
|
|
187
|
+
return json(res, 200, await chars.renamePack(b.pack, b.name));
|
|
188
|
+
}
|
|
189
|
+
case "POST /api/packs/delete":
|
|
190
|
+
return json(res, 200, await chars.deletePack((await body()).pack));
|
|
191
|
+
case "POST /api/packs/emoji": {
|
|
192
|
+
const b = await body();
|
|
193
|
+
return json(res, 200, await chars.addEmoji(b.pack, b.emoji));
|
|
194
|
+
}
|
|
195
|
+
case "POST /api/packs/image": {
|
|
196
|
+
const b = await body();
|
|
197
|
+
return json(res, 200, await chars.addImage(b.pack, b.name, b.dataURL));
|
|
198
|
+
}
|
|
199
|
+
case "POST /api/packs/remove": {
|
|
200
|
+
const b = await body();
|
|
201
|
+
return json(res, 200, await chars.removeCharacter(b.pack, b.id));
|
|
202
|
+
}
|
|
124
203
|
case "GET /api/sessions":
|
|
125
204
|
return json(res, 200, opts.hub.sessions());
|
|
126
205
|
case "GET /api/events":
|
|
@@ -145,6 +224,38 @@ export function createAppServer(opts) {
|
|
|
145
224
|
res.writeHead(204).end();
|
|
146
225
|
return;
|
|
147
226
|
}
|
|
227
|
+
case "PUT /api/files": {
|
|
228
|
+
const body = (await readJson(req));
|
|
229
|
+
if (!Array.isArray(body?.files))
|
|
230
|
+
throw new HttpError(400, "files must be an array");
|
|
231
|
+
const bad = body.files.map(validFile).find((f) => f instanceof Error);
|
|
232
|
+
if (bad)
|
|
233
|
+
throw new HttpError(400, bad.message);
|
|
234
|
+
await opts.scenes.saveFiles(body.files);
|
|
235
|
+
res.writeHead(204).end();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
case "POST /api/files/get": {
|
|
239
|
+
const body = (await readJson(req));
|
|
240
|
+
if (!Array.isArray(body?.ids) || !body.ids.every((i) => typeof i === "string")) {
|
|
241
|
+
throw new HttpError(400, "ids must be a string array");
|
|
242
|
+
}
|
|
243
|
+
return json(res, 200, await opts.scenes.loadFiles(body.ids));
|
|
244
|
+
}
|
|
245
|
+
case "POST /api/end": {
|
|
246
|
+
const body = (await readJson(req));
|
|
247
|
+
if (typeof body?.sessionId !== "string" || !SESSION_ID.test(body.sessionId)) {
|
|
248
|
+
throw new HttpError(400, "sessionId must be a session id");
|
|
249
|
+
}
|
|
250
|
+
const result = await opts.hub.end(body.sessionId);
|
|
251
|
+
if (result === "ended") {
|
|
252
|
+
res.writeHead(204).end();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (result === "unsupported")
|
|
256
|
+
return json(res, 501, { error: "unsupported" });
|
|
257
|
+
return json(res, 409, { error: "not running" });
|
|
258
|
+
}
|
|
148
259
|
case "POST /api/focus": {
|
|
149
260
|
const body = (await readJson(req));
|
|
150
261
|
if (typeof body?.sessionId !== "string")
|
|
@@ -158,7 +269,7 @@ export function createAppServer(opts) {
|
|
|
158
269
|
case "POST /api/resume": {
|
|
159
270
|
const body = (await readJson(req));
|
|
160
271
|
if (typeof body?.sessionId !== "string" || !SESSION_ID.test(body.sessionId)) {
|
|
161
|
-
throw new HttpError(400, "sessionId must be a session
|
|
272
|
+
throw new HttpError(400, "sessionId must be a session id");
|
|
162
273
|
}
|
|
163
274
|
const result = await opts.hub.resume(body.sessionId);
|
|
164
275
|
if (result === "resumed") {
|
|
@@ -189,23 +300,23 @@ export function createAppServer(opts) {
|
|
|
189
300
|
return text(res, 401, "bad token");
|
|
190
301
|
res.writeHead(302, {
|
|
191
302
|
location: "/",
|
|
192
|
-
"set-cookie": `${
|
|
303
|
+
"set-cookie": `${cookieName(port)}=${opts.token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${COOKIE_MAX_AGE}`,
|
|
193
304
|
});
|
|
194
305
|
res.end();
|
|
195
306
|
return;
|
|
196
307
|
}
|
|
197
|
-
if (!sameToken(cookieToken(req), opts.token)) {
|
|
308
|
+
if (!sameToken(cookieToken(req, port), opts.token)) {
|
|
198
309
|
return text(res, 401, "excaliagent: open the URL printed in the terminal (it carries the access token).");
|
|
199
310
|
}
|
|
200
311
|
if (url.pathname.startsWith("/api/"))
|
|
201
|
-
return api(req, res, url.pathname);
|
|
312
|
+
return api(req, res, url.pathname, url.searchParams);
|
|
202
313
|
if (opts.middleware)
|
|
203
314
|
return opts.middleware(req, res, () => text(res, 404, "not found"));
|
|
204
315
|
return serveStatic(req, res, url.pathname);
|
|
205
316
|
}
|
|
206
317
|
const server = createServer((req, res) => {
|
|
207
318
|
handle(req, res).catch((err) => {
|
|
208
|
-
const status = err instanceof HttpError ? err.status : 500;
|
|
319
|
+
const status = err instanceof HttpError || err instanceof StatusError ? err.status : 500;
|
|
209
320
|
if (status === 500)
|
|
210
321
|
console.error("excaliagent:", err);
|
|
211
322
|
if (!res.headersSent)
|
package/dist/server/hub.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The id formats of every supported harness (Claude and Codex: UUIDs;
|
|
3
|
+
* OpenCode: `ses_` + 26 alphanumerics).
|
|
4
|
+
* Anything else is refused before it reaches a source or the characters store;
|
|
5
|
+
* each source also checks its own exact format before an id goes into a command.
|
|
6
|
+
*/
|
|
7
|
+
export const SESSION_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|ses_[0-9a-zA-Z]{26})$/i;
|
|
3
8
|
const LABEL_MAX = 40;
|
|
4
9
|
function resumeLabel(s) {
|
|
5
10
|
const title = s.title?.trim();
|
|
@@ -80,16 +85,25 @@ export function createHub({ source, panes }) {
|
|
|
80
85
|
return "unknown";
|
|
81
86
|
if (s.live)
|
|
82
87
|
return "live";
|
|
88
|
+
if (!s.resumeCommand)
|
|
89
|
+
return "unknown";
|
|
83
90
|
if (!panes.resume || !panes.available())
|
|
84
91
|
return "no-herdr";
|
|
85
92
|
await panes.resume({
|
|
86
93
|
cwd: s.cwd,
|
|
87
94
|
...(s.repoRoot !== undefined ? { repoRoot: s.repoRoot } : {}),
|
|
88
|
-
command:
|
|
95
|
+
command: s.resumeCommand,
|
|
89
96
|
label: resumeLabel(s),
|
|
90
97
|
});
|
|
91
98
|
return "resumed";
|
|
92
99
|
},
|
|
100
|
+
async end(sessionId) {
|
|
101
|
+
if (!SESSION_ID.test(sessionId))
|
|
102
|
+
return "unknown";
|
|
103
|
+
if (!source.end)
|
|
104
|
+
return "unsupported";
|
|
105
|
+
return source.end(sessionId);
|
|
106
|
+
},
|
|
93
107
|
async focus(sessionId) {
|
|
94
108
|
const pane = current.find((s) => s.id === sessionId)?.pane;
|
|
95
109
|
if (!pane)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// One board per state dir: two servers saving the same scene.json would
|
|
2
|
+
// overwrite each other's edits. A running server records its port and pid in
|
|
3
|
+
// <stateDir>/server.json; a second start finds it (pid alive, and it answers to
|
|
4
|
+
// this state dir's token) and opens that board instead of starting another.
|
|
5
|
+
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { cookieName } from "./http.js";
|
|
8
|
+
const recordPath = (stateDir) => join(stateDir, "server.json");
|
|
9
|
+
export async function recordRunning(stateDir, port, pid = process.pid) {
|
|
10
|
+
await writeFile(recordPath(stateDir), JSON.stringify({ port, pid }), { mode: 0o600 });
|
|
11
|
+
}
|
|
12
|
+
/** Removes the record if it is `pid`'s (a newer server may have replaced it). */
|
|
13
|
+
export async function clearRunning(stateDir, pid = process.pid) {
|
|
14
|
+
const r = await readRecord(stateDir);
|
|
15
|
+
if (r?.pid === pid)
|
|
16
|
+
await rm(recordPath(stateDir), { force: true });
|
|
17
|
+
}
|
|
18
|
+
async function readRecord(stateDir) {
|
|
19
|
+
try {
|
|
20
|
+
const r = JSON.parse(await readFile(recordPath(stateDir), "utf8"));
|
|
21
|
+
return Number.isInteger(r.port) && Number.isInteger(r.pid) ? r : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function alive(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return e.code === "EPERM";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The port of a live board serving this state dir, if any. */
|
|
37
|
+
export async function findRunning(stateDir, token) {
|
|
38
|
+
const r = await readRecord(stateDir);
|
|
39
|
+
if (!r || !alive(r.pid))
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`http://127.0.0.1:${r.port}/api/sessions`, {
|
|
43
|
+
headers: { cookie: `${cookieName(r.port)}=${token}` },
|
|
44
|
+
signal: AbortSignal.timeout(1500),
|
|
45
|
+
});
|
|
46
|
+
await res.body?.cancel();
|
|
47
|
+
return res.ok ? r.port : undefined;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -50,15 +50,40 @@ export function defaultHerdrSocketPath() {
|
|
|
50
50
|
return herdrSocketPath(process.env, homedir()) ?? join(homedir(), ".config", "herdr", "herdr.sock");
|
|
51
51
|
}
|
|
52
52
|
const workspaceOf = (paneId) => paneId.split(":")[0] ?? paneId;
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* pid → pane for every foreground process of every pane; `panes` supplies ids
|
|
55
|
+
* and labels. With `procs` (the process table), every descendant of a pane's
|
|
56
|
+
* shell maps to that pane too: a session is not always in the foreground (a
|
|
57
|
+
* command it runs can take the terminal, or it is suspended). Foreground wins.
|
|
58
|
+
*/
|
|
59
|
+
export function parsePaneMap(infos, panes = new Map(), procs = []) {
|
|
55
60
|
const map = new Map();
|
|
61
|
+
const byShell = new Map();
|
|
56
62
|
for (const info of infos) {
|
|
57
63
|
const pane = panes.get(info.pane_id) ?? { paneId: info.pane_id, workspaceId: workspaceOf(info.pane_id) };
|
|
58
64
|
for (const p of info.foreground_processes ?? []) {
|
|
59
65
|
if (typeof p.pid === "number")
|
|
60
66
|
map.set(p.pid, pane);
|
|
61
67
|
}
|
|
68
|
+
if (typeof info.shell_pid === "number")
|
|
69
|
+
byShell.set(info.shell_pid, pane);
|
|
70
|
+
}
|
|
71
|
+
if (!byShell.size)
|
|
72
|
+
return map;
|
|
73
|
+
const children = new Map();
|
|
74
|
+
for (const p of procs)
|
|
75
|
+
children.set(p.ppid, [...(children.get(p.ppid) ?? []), p.pid]);
|
|
76
|
+
const seen = new Set(); // a pid is walked once, even if the table has a cycle
|
|
77
|
+
for (const [shell, pane] of byShell) {
|
|
78
|
+
const todo = [...(children.get(shell) ?? [])];
|
|
79
|
+
for (let pid = todo.pop(); pid !== undefined; pid = todo.pop()) {
|
|
80
|
+
if (seen.has(pid))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(pid);
|
|
83
|
+
if (!map.has(pid))
|
|
84
|
+
map.set(pid, pane);
|
|
85
|
+
todo.push(...(children.get(pid) ?? []));
|
|
86
|
+
}
|
|
62
87
|
}
|
|
63
88
|
return map;
|
|
64
89
|
}
|
|
@@ -150,6 +175,7 @@ export function createHerdrLink(opts = {}) {
|
|
|
150
175
|
panes.set(p.pane_id, pane);
|
|
151
176
|
}
|
|
152
177
|
const panesList = list;
|
|
178
|
+
const procs = table.list().catch(() => []);
|
|
153
179
|
const infos = await Promise.all(panesList.map(async (p) => {
|
|
154
180
|
try {
|
|
155
181
|
const res = (await client.request("pane.process_info", { pane_id: p.pane_id }));
|
|
@@ -161,7 +187,7 @@ export function createHerdrLink(opts = {}) {
|
|
|
161
187
|
}));
|
|
162
188
|
if (closed || !client.connected())
|
|
163
189
|
return;
|
|
164
|
-
setMap(parsePaneMap(infos.filter((i) => !!i), panes));
|
|
190
|
+
setMap(parsePaneMap(infos.filter((i) => !!i), panes, await procs));
|
|
165
191
|
}
|
|
166
192
|
// Coalesce: at most one build running, plus one queued behind it.
|
|
167
193
|
let running;
|
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rename } from "node:fs/promises";
|
|
1
|
+
import { mkdir, open, readFile, rename, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
/** Excalidraw file ids: hex hashes or nanoids. Also keeps paths inside files/. */
|
|
5
|
+
export const FILE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
6
|
+
const IMAGE_MIME = /^image\/[a-z0-9.+-]+$/;
|
|
7
|
+
/** A clean StoredFile, or an Error naming the bad field. */
|
|
8
|
+
export function validFile(f) {
|
|
9
|
+
const x = f;
|
|
10
|
+
if (typeof x?.id !== "string" || !FILE_ID.test(x.id))
|
|
11
|
+
return new Error("file id must match " + FILE_ID);
|
|
12
|
+
if (typeof x.mimeType !== "string" || !IMAGE_MIME.test(x.mimeType))
|
|
13
|
+
return new Error("file mimeType must be image/*");
|
|
14
|
+
if (typeof x.dataURL !== "string" || !x.dataURL.startsWith(`data:${x.mimeType}`)) {
|
|
15
|
+
return new Error("file dataURL must be a data: URL of its mimeType");
|
|
16
|
+
}
|
|
17
|
+
return { id: x.id, mimeType: x.mimeType, dataURL: x.dataURL, created: typeof x.created === "number" ? x.created : 0 };
|
|
18
|
+
}
|
|
4
19
|
export function defaultStateDir(env = process.env, home = homedir()) {
|
|
5
20
|
const base = env.XDG_STATE_HOME || join(home, ".local", "state");
|
|
6
21
|
return join(base, "excaliagent");
|
|
7
22
|
}
|
|
8
23
|
const empty = () => ({ elements: [], appState: {} });
|
|
9
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* scene.json in stateDir, written atomically (temp file + fsync + rename), saves
|
|
26
|
+
* serialized. Image files live apart, one files/<id>.json each, written once.
|
|
27
|
+
*/
|
|
10
28
|
export function createSceneStore(stateDir, log = console) {
|
|
11
29
|
const path = join(stateDir, "scene.json");
|
|
12
30
|
let queue = Promise.resolve();
|
|
13
31
|
let seq = 0;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const tmp = `${
|
|
32
|
+
const filesDir = join(stateDir, "files");
|
|
33
|
+
async function writeAtomic(target, data) {
|
|
34
|
+
const tmp = `${target}.tmp-${process.pid}-${seq++}`;
|
|
17
35
|
const fh = await open(tmp, "w", 0o600);
|
|
18
36
|
try {
|
|
19
|
-
await fh.writeFile(JSON.stringify(
|
|
37
|
+
await fh.writeFile(JSON.stringify(data));
|
|
20
38
|
await fh.sync();
|
|
21
39
|
}
|
|
22
40
|
finally {
|
|
23
41
|
await fh.close();
|
|
24
42
|
}
|
|
25
|
-
await rename(tmp,
|
|
43
|
+
await rename(tmp, target);
|
|
44
|
+
}
|
|
45
|
+
async function write(scene) {
|
|
46
|
+
await mkdir(stateDir, { recursive: true });
|
|
47
|
+
await writeAtomic(path, scene);
|
|
26
48
|
}
|
|
27
49
|
return {
|
|
28
50
|
path,
|
|
@@ -57,5 +79,34 @@ export function createSceneStore(stateDir, log = console) {
|
|
|
57
79
|
queue = run.catch(() => { });
|
|
58
80
|
return run;
|
|
59
81
|
},
|
|
82
|
+
async saveFiles(files) {
|
|
83
|
+
const valid = files.map(validFile);
|
|
84
|
+
const bad = valid.find((f) => f instanceof Error);
|
|
85
|
+
if (bad)
|
|
86
|
+
throw bad;
|
|
87
|
+
await mkdir(filesDir, { recursive: true });
|
|
88
|
+
for (const f of valid) {
|
|
89
|
+
const target = join(filesDir, `${f.id}.json`);
|
|
90
|
+
if (await stat(target).catch(() => undefined))
|
|
91
|
+
continue;
|
|
92
|
+
await writeAtomic(target, f);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
async loadFiles(ids) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
if (!FILE_ID.test(id))
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
const f = validFile(JSON.parse(await readFile(join(filesDir, `${id}.json`), "utf8")));
|
|
102
|
+
if (!(f instanceof Error) && f.id === id)
|
|
103
|
+
out.push(f);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// missing or unreadable: the image shows as a placeholder
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
},
|
|
60
111
|
};
|
|
61
112
|
}
|
|
@@ -5,10 +5,23 @@ import { watch as fsWatch } from "node:fs";
|
|
|
5
5
|
import { readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { basename, join } from "node:path";
|
|
8
|
+
import { createProcessTable } from "../desktop/process-table.js";
|
|
8
9
|
import { GitReader } from "./git.js";
|
|
9
10
|
import { onceLogger } from "./log.js";
|
|
10
11
|
import { defaultIsAlive, isLive, readRegistry } from "./registry.js";
|
|
11
12
|
import { TranscriptScanner } from "./transcript.js";
|
|
13
|
+
/** A Claude Code process: its executable, argv[0], or script (node cli.js) names claude. */
|
|
14
|
+
export function looksLikeClaude(argv, exe) {
|
|
15
|
+
const names = [argv?.[0] && basename(argv[0]), exe && basename(exe)];
|
|
16
|
+
return names.some((n) => !!n && /claude/i.test(n)) || /claude/i.test(argv?.[1] ?? "");
|
|
17
|
+
}
|
|
18
|
+
function defaultIsClaudeProcess() {
|
|
19
|
+
const table = createProcessTable();
|
|
20
|
+
return async (pid) => looksLikeClaude(await table.argv(pid), await table.exe(pid));
|
|
21
|
+
}
|
|
22
|
+
/** Claude session ids are UUIDs; anything else is never put into a command. */
|
|
23
|
+
export const CLAUDE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
export const claudeResumeCommand = (id) => `claude --resume ${id}`;
|
|
12
25
|
const LIVE_STATUSES = new Set([
|
|
13
26
|
"busy", "idle", "waiting", "needs_user", "needs_trust", "blocked",
|
|
14
27
|
]);
|
|
@@ -26,6 +39,8 @@ export function liveSession(e, t, git) {
|
|
|
26
39
|
live: true,
|
|
27
40
|
pid: e.pid,
|
|
28
41
|
prLinks: t?.prLinks ?? [],
|
|
42
|
+
resumeCommand: claudeResumeCommand(e.sessionId),
|
|
43
|
+
canEnd: true,
|
|
29
44
|
};
|
|
30
45
|
return withGit(withTranscriptExtras(s, t, e.name, e.waitingFor), git);
|
|
31
46
|
}
|
|
@@ -42,6 +57,7 @@ export function dormantSession(f, t, fallbackCwd, git) {
|
|
|
42
57
|
cacheExpiresAt: t.cacheExpiresAt,
|
|
43
58
|
live: false,
|
|
44
59
|
prLinks: t.prLinks,
|
|
60
|
+
resumeCommand: claudeResumeCommand(f.id),
|
|
45
61
|
};
|
|
46
62
|
// A parked session keeps the branch it was on; only the useless "HEAD" stamp
|
|
47
63
|
// is replaced by the repo's current branch.
|
|
@@ -97,6 +113,8 @@ export function createClaudeSource(opts = {}) {
|
|
|
97
113
|
const warn = onceLogger(opts.log ?? console.warn);
|
|
98
114
|
const scanner = new TranscriptScanner({ warn });
|
|
99
115
|
const git = new GitReader({ ttlMs: opts.gitTtlMs ?? 2000 });
|
|
116
|
+
const isClaudeProcess = opts.isClaudeProcess ?? defaultIsClaudeProcess();
|
|
117
|
+
const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
100
118
|
const pathById = new Map();
|
|
101
119
|
/** A cwd seen in any transcript of a project dir, for transcripts without one. */
|
|
102
120
|
const cwdBySlug = new Map();
|
|
@@ -197,6 +215,8 @@ export function createClaudeSource(opts = {}) {
|
|
|
197
215
|
return toDormant(files, maxDormant);
|
|
198
216
|
}
|
|
199
217
|
async function get(id) {
|
|
218
|
+
if (!CLAUDE_ID.test(id))
|
|
219
|
+
return undefined;
|
|
200
220
|
const live = (await list()).find((s) => s.id === id);
|
|
201
221
|
if (live)
|
|
202
222
|
return live;
|
|
@@ -288,5 +308,20 @@ export function createClaudeSource(opts = {}) {
|
|
|
288
308
|
watcher = undefined;
|
|
289
309
|
};
|
|
290
310
|
}
|
|
291
|
-
|
|
311
|
+
/** SIGTERM, as when its terminal closes: Claude Code exits and keeps the transcript. */
|
|
312
|
+
async function end(id) {
|
|
313
|
+
if (!CLAUDE_ID.test(id))
|
|
314
|
+
return "not-live";
|
|
315
|
+
const e = (await liveEntries()).find((x) => x.sessionId === id);
|
|
316
|
+
if (!e || !(await isClaudeProcess(e.pid).catch(() => false)))
|
|
317
|
+
return "not-live";
|
|
318
|
+
try {
|
|
319
|
+
kill(e.pid, "SIGTERM");
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return "not-live"; // exited in the meantime
|
|
323
|
+
}
|
|
324
|
+
return "ended";
|
|
325
|
+
}
|
|
326
|
+
return { list, listDormant, get, watch, end };
|
|
292
327
|
}
|