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.
Files changed (168) hide show
  1. package/README.md +129 -30
  2. package/dist/server/args.js +19 -3
  3. package/dist/server/characters.js +540 -0
  4. package/dist/server/cli.js +47 -6
  5. package/dist/server/http.js +122 -11
  6. package/dist/server/hub.js +17 -3
  7. package/dist/server/instance.js +52 -0
  8. package/dist/server/panes/herdr.js +29 -3
  9. package/dist/server/scene-store.js +58 -7
  10. package/dist/server/sources/claude.js +36 -1
  11. package/dist/server/sources/codex-live.js +279 -0
  12. package/dist/server/sources/codex-rollout.js +154 -0
  13. package/dist/server/sources/codex.js +363 -0
  14. package/dist/server/sources/jsonl.js +142 -0
  15. package/dist/server/sources/multi.js +53 -0
  16. package/dist/server/sources/opencode-live.js +144 -0
  17. package/dist/server/sources/opencode.js +265 -0
  18. package/dist/server/sources/proc.js +34 -0
  19. package/dist/server/sources/transcript.js +9 -103
  20. package/dist/server/token.js +23 -0
  21. package/dist/shared/packs.js +62 -0
  22. package/dist/web/apple-touch-icon.png +0 -0
  23. package/dist/web/assets/{abnfDiagram-VCTEODGH-tmjYj3VY.js → abnfDiagram-VCTEODGH-BxrKsDoB.js} +1 -1
  24. package/dist/web/assets/{ar-SA-G6X2FPQ2-8ZGGBDRT.js → ar-SA-G6X2FPQ2-Cc6gbg88.js} +1 -1
  25. package/dist/web/assets/{architecture-7GRP2DOG-CsvDkWt3.js → architecture-7GRP2DOG-CIqN5I9B.js} +1 -1
  26. package/dist/web/assets/{architectureDiagram-5GKGNRK7-Bq-1CDhU.js → architectureDiagram-5GKGNRK7-DsXC8AqK.js} +1 -1
  27. package/dist/web/assets/{az-AZ-76LH7QW2-DbdS_q-w.js → az-AZ-76LH7QW2-BH8VzNec.js} +1 -1
  28. package/dist/web/assets/{bg-BG-XCXSNQG7-CANqXhG1.js → bg-BG-XCXSNQG7-ClFUbics.js} +1 -1
  29. package/dist/web/assets/{blockDiagram-I7D4REHJ-DtYn-xOl.js → blockDiagram-I7D4REHJ-CltS-Zt2.js} +1 -1
  30. package/dist/web/assets/{bn-BD-2XOGV67Q-DJdq0B1R.js → bn-BD-2XOGV67Q-BVUiOO4B.js} +1 -1
  31. package/dist/web/assets/{c4Diagram-7LVT6UL2-DW6Pzj1e.js → c4Diagram-7LVT6UL2-C-C_SvWm.js} +1 -1
  32. package/dist/web/assets/{ca-ES-6MX7JW3Y-B7pUqerV.js → ca-ES-6MX7JW3Y-CEK8mB8J.js} +1 -1
  33. package/dist/web/assets/channel-Dom8D_0o.js +1 -0
  34. package/dist/web/assets/{chunk-4HAMMTFA-_AKNu1--.js → chunk-4HAMMTFA-Bfdmb0Hg.js} +1 -1
  35. package/dist/web/assets/{chunk-75Z2AOVW-BoMl8Mqw.js → chunk-75Z2AOVW-NYwkutzS.js} +1 -1
  36. package/dist/web/assets/{chunk-DU6HZSFF-DYHa8ENH.js → chunk-DU6HZSFF-SqE0xdu5.js} +1 -1
  37. package/dist/web/assets/{chunk-EIO257PC--adgQBo2.js → chunk-EIO257PC-DpOeKaLq.js} +1 -1
  38. package/dist/web/assets/{chunk-F27PBJKO-CvQi01qZ.js → chunk-F27PBJKO-g9bJrSwZ.js} +1 -1
  39. package/dist/web/assets/{chunk-GMAD6QVW-ojaxPuMG.js → chunk-GMAD6QVW-B1y2ol5w.js} +1 -1
  40. package/dist/web/assets/{chunk-GVQU2GXP-DYAeD1IL.js → chunk-GVQU2GXP-BUjRNRXo.js} +1 -1
  41. package/dist/web/assets/{chunk-IMKFNOWR-oVPWqbpV.js → chunk-IMKFNOWR-DyeQep8U.js} +1 -1
  42. package/dist/web/assets/{chunk-L3NEJ4N5-BelAXiIN.js → chunk-L3NEJ4N5-BcTVhsWp.js} +1 -1
  43. package/dist/web/assets/{chunk-OSK3NFVY-CATtjN8l.js → chunk-OSK3NFVY-Db7B8jPm.js} +1 -1
  44. package/dist/web/assets/{chunk-P2QGCYS3-CbKPCENJ.js → chunk-P2QGCYS3-Cx5iBC5z.js} +1 -1
  45. package/dist/web/assets/{chunk-POPQ4Y6H-BfOCSezn.js → chunk-POPQ4Y6H-CqVRZDA8.js} +1 -1
  46. package/dist/web/assets/{chunk-PWAF6VOD-C7O-JWJt.js → chunk-PWAF6VOD-Cf2j9yew.js} +1 -1
  47. package/dist/web/assets/{chunk-SHT3W25Y-BfEpPCTX.js → chunk-SHT3W25Y-0_N13xgE.js} +1 -1
  48. package/dist/web/assets/{chunk-SVP7TREG-CaeKcCSu.js → chunk-SVP7TREG-DkRJF9jr.js} +1 -1
  49. package/dist/web/assets/{chunk-TICWLB2K-Bx6AcLB_.js → chunk-TICWLB2K-D6OC50xH.js} +1 -1
  50. package/dist/web/assets/classDiagram-ZZMXUADV-BMEkgkEJ.js +1 -0
  51. package/dist/web/assets/classDiagram-v2-VYDZK3BY-BMEkgkEJ.js +1 -0
  52. package/dist/web/assets/{cs-CZ-2BRQDIVT-DF8hZ2mh.js → cs-CZ-2BRQDIVT-DWt1RUsP.js} +1 -1
  53. package/dist/web/assets/{cynefin-OW5HDTMX-Dol6Weuo.js → cynefin-OW5HDTMX-Dx0HzRCl.js} +1 -1
  54. package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-CbOJ2lks.js → cynefinDiagram-5FMLGOSQ-C8AMQNhe.js} +1 -1
  55. package/dist/web/assets/{da-DK-5WZEPLOC-CgPdNz6E.js → da-DK-5WZEPLOC-BPggh0j6.js} +1 -1
  56. package/dist/web/assets/{dagre-GXQ25YYZ-CcuaBID7.js → dagre-GXQ25YYZ-f9qU6BFq.js} +1 -1
  57. package/dist/web/assets/{de-DE-XR44H4JA-DaVeYWRc.js → de-DE-XR44H4JA-B1q1pJrQ.js} +1 -1
  58. package/dist/web/assets/{diagram-S7CK7UJ4-BR2gIsNx.js → diagram-S7CK7UJ4-Bcq77Nl3.js} +1 -1
  59. package/dist/web/assets/{diagram-UQ7AKVKN-DwSZsV-Z.js → diagram-UQ7AKVKN-DXzlgVTk.js} +1 -1
  60. package/dist/web/assets/{diagram-VSXAHHWV-BcJpQHbj.js → diagram-VSXAHHWV-C1vz2FvT.js} +1 -1
  61. package/dist/web/assets/{diagram-VX7I27RA-CvjRTkwu.js → diagram-VX7I27RA-_c4ucp3r.js} +1 -1
  62. package/dist/web/assets/{diagram-Z3DM3KII-vg0-r_P4.js → diagram-Z3DM3KII-cUg91K-F.js} +1 -1
  63. package/dist/web/assets/{dist-D-3cz42D.js → dist-DcwmvBdV.js} +4 -4
  64. package/dist/web/assets/{ebnfDiagram-PWID7BFC-DZCaMmMd.js → ebnfDiagram-PWID7BFC-9iWfohHg.js} +1 -1
  65. package/dist/web/assets/{el-GR-BZB4AONW-nYncjO-L.js → el-GR-BZB4AONW-CrhwvNDD.js} +1 -1
  66. package/dist/web/assets/{en-B4ZKOASM-DqIz0rJz.js → en-B4ZKOASM-DCjnkz5L.js} +1 -1
  67. package/dist/web/assets/{erDiagram-RLTQ6QDP-DgyrjoaZ.js → erDiagram-RLTQ6QDP-A72uVlS3.js} +1 -1
  68. package/dist/web/assets/{es-ES-U4NZUMDT-e3WeRDLl.js → es-ES-U4NZUMDT-D0sRCoDL.js} +1 -1
  69. package/dist/web/assets/{eu-ES-A7QVB2H4-CwXv1t13.js → eu-ES-A7QVB2H4-DLG-liPb.js} +1 -1
  70. package/dist/web/assets/{eventmodeling-NTZA5JFV-DCTWpAwP.js → eventmodeling-NTZA5JFV-CIKZvDAj.js} +1 -1
  71. package/dist/web/assets/{fa-IR-HGAKTJCU-D-9plwKu.js → fa-IR-HGAKTJCU-BW3pPOVT.js} +1 -1
  72. package/dist/web/assets/{fi-FI-Z5N7JZ37-BTK1rs8G.js → fi-FI-Z5N7JZ37-D2XaqwPp.js} +1 -1
  73. package/dist/web/assets/flowDiagram-HODETNUW-DhQSc3KP.js +1 -0
  74. package/dist/web/assets/{fr-FR-RHASNOE6-CAuUMPtN.js → fr-FR-RHASNOE6-B-BjRA7r.js} +1 -1
  75. package/dist/web/assets/{ganttDiagram-EL5Y4UJY-bFfu4BMj.js → ganttDiagram-EL5Y4UJY-cgwUfr0G.js} +1 -1
  76. package/dist/web/assets/{gitGraph-4MIJSDKK-DIl4-oqK.js → gitGraph-4MIJSDKK-CeKopPn3.js} +1 -1
  77. package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-ho3FT8XP.js → gitGraphDiagram-WWUBYQGX-BWjAy7ei.js} +1 -1
  78. package/dist/web/assets/{gl-ES-HMX3MZ6V-DlKPdjtk.js → gl-ES-HMX3MZ6V-DYUAqB0N.js} +1 -1
  79. package/dist/web/assets/{he-IL-6SHJWFNN-CLyP0zBo.js → he-IL-6SHJWFNN-CXkYqbuu.js} +1 -1
  80. package/dist/web/assets/{hi-IN-IWLTKZ5I-kvhBHNzH.js → hi-IN-IWLTKZ5I-BwVBsHWG.js} +1 -1
  81. package/dist/web/assets/{hu-HU-A5ZG7DT2-Dtd-o-RJ.js → hu-HU-A5ZG7DT2-DkmlE7Aa.js} +1 -1
  82. package/dist/web/assets/{id-ID-SAP4L64H-VZ3IV-8B.js → id-ID-SAP4L64H-CwkytHX_.js} +1 -1
  83. package/dist/web/assets/{image-GAAHSSAO-DWvjHHd3.js → image-GAAHSSAO-u-hBxkaK.js} +1 -1
  84. package/dist/web/assets/index-D43I17T9.js +212 -0
  85. package/dist/web/assets/{index-DtRVLazd.css → index-DxXUknKX.css} +1 -1
  86. package/dist/web/assets/{info-A6RAGUB7-DN8pwhKt.js → info-A6RAGUB7-DmadBmci.js} +1 -1
  87. package/dist/web/assets/{infoDiagram-27XIBGKW-i2JHBJKe.js → infoDiagram-27XIBGKW-D93CKwhy.js} +1 -1
  88. package/dist/web/assets/{ishikawaDiagram-5VMMS53U-DLLdu9-W.js → ishikawaDiagram-5VMMS53U-BQ-x5YXp.js} +1 -1
  89. package/dist/web/assets/{it-IT-JPQ66NNP-6SULWnRA.js → it-IT-JPQ66NNP-Bc-Qa6KI.js} +1 -1
  90. package/dist/web/assets/{ja-JP-DBVTYXUO-BbgutQYO.js → ja-JP-DBVTYXUO-BPmQCryQ.js} +1 -1
  91. package/dist/web/assets/{journeyDiagram-3NMN7TZE-DsysKJxX.js → journeyDiagram-3NMN7TZE-CO_LXx1E.js} +1 -1
  92. package/dist/web/assets/{kaa-6HZHGXH3-Bc6yhZWQ.js → kaa-6HZHGXH3-DuBFVAjv.js} +1 -1
  93. package/dist/web/assets/{kab-KAB-ZGHBKWFO-BaICxis-.js → kab-KAB-ZGHBKWFO-DhEFzSTh.js} +1 -1
  94. package/dist/web/assets/{kanban-definition-UXKFOSKX-w3k_6RhT.js → kanban-definition-UXKFOSKX-B72Jztau.js} +1 -1
  95. package/dist/web/assets/{kk-KZ-P5N5QNE5-BVsfmPoG.js → kk-KZ-P5N5QNE5-CW7PxU2h.js} +1 -1
  96. package/dist/web/assets/{km-KH-HSX4SM5Z-BS1M1bUx.js → km-KH-HSX4SM5Z-CYy7yySD.js} +1 -1
  97. package/dist/web/assets/{ko-KR-MTYHY66A-C0x16zQm.js → ko-KR-MTYHY66A-C5O645Qe.js} +1 -1
  98. package/dist/web/assets/{ku-TR-6OUDTVRD-BWI9z5E8.js → ku-TR-6OUDTVRD-CKcd_iWr.js} +1 -1
  99. package/dist/web/assets/{line-BRXlgUrO.js → line-Ckd3sAmP.js} +1 -1
  100. package/dist/web/assets/{lt-LT-XHIRWOB4-CTDq1zFR.js → lt-LT-XHIRWOB4-ya2_xWiy.js} +1 -1
  101. package/dist/web/assets/{lv-LV-5QDEKY6T-h8b-E15B.js → lv-LV-5QDEKY6T-cYYwyzmz.js} +1 -1
  102. package/dist/web/assets/{mermaid-parser.core-Da_XEVmR.js → mermaid-parser.core-Dbqkvrw-.js} +3 -3
  103. package/dist/web/assets/{mindmap-definition-YA3MSWOX-DUBdgu2g.js → mindmap-definition-YA3MSWOX-B4onxNgk.js} +1 -1
  104. package/dist/web/assets/{mr-IN-CRQNXWMA-dTdkz007.js → mr-IN-CRQNXWMA-CfaHpVWe.js} +1 -1
  105. package/dist/web/assets/{my-MM-5M5IBNSE-CmYwTRil.js → my-MM-5M5IBNSE-BfKiTATq.js} +1 -1
  106. package/dist/web/assets/{nb-NO-T6EIAALU-DLLYcK-s.js → nb-NO-T6EIAALU-Cz3yDFYg.js} +1 -1
  107. package/dist/web/assets/{nl-NL-IS3SIHDZ-mtv15Mmm.js → nl-NL-IS3SIHDZ-DDX026n5.js} +1 -1
  108. package/dist/web/assets/{nn-NO-6E72VCQL-CJDTT3kB.js → nn-NO-6E72VCQL-BcokL_SN.js} +1 -1
  109. package/dist/web/assets/{oc-FR-POXYY2M6-DdHJTqH4.js → oc-FR-POXYY2M6-bNFsGpEp.js} +1 -1
  110. package/dist/web/assets/{pa-IN-N4M65BXN-DxzzhbLu.js → pa-IN-N4M65BXN-iIHN9Z8W.js} +1 -1
  111. package/dist/web/assets/{packet-AYTQ26CC-CzX42SOE.js → packet-AYTQ26CC-BzBpP9hg.js} +1 -1
  112. package/dist/web/assets/{pegDiagram-XKGWAZYB-1R2Hd3Th.js → pegDiagram-XKGWAZYB-BPScRPVZ.js} +1 -1
  113. package/dist/web/assets/percentages-BXMCSKIN-EDfR-TFr.js +1 -0
  114. package/dist/web/assets/{pie-WAS4IAKB-DEI2uslz.js → pie-WAS4IAKB-N-9RHtKL.js} +1 -1
  115. package/dist/web/assets/{pieDiagram-E7YTZNPT-DOQnFSrs.js → pieDiagram-E7YTZNPT-Bpd_yCG-.js} +1 -1
  116. package/dist/web/assets/{pl-PL-T2D74RX3-Dpyab0lI.js → pl-PL-T2D74RX3-DeqOx-a4.js} +1 -1
  117. package/dist/web/assets/{pt-BR-5N22H2LF-DOGrUA6Y.js → pt-BR-5N22H2LF-DG1PCIkb.js} +1 -1
  118. package/dist/web/assets/{pt-PT-UZXXM6DQ-DAoNiiOg.js → pt-PT-UZXXM6DQ-BsUJk2fq.js} +1 -1
  119. package/dist/web/assets/{quadrantDiagram-AXDQQJYC-CQE1OxRJ.js → quadrantDiagram-AXDQQJYC-CSLCp0xS.js} +1 -1
  120. package/dist/web/assets/{radar-RG4KPBEZ-B-er2ORT.js → radar-RG4KPBEZ-rKULJroO.js} +1 -1
  121. package/dist/web/assets/{railroad-74A4TZTK-D1Veuu81.js → railroad-74A4TZTK-hDn0WPg3.js} +1 -1
  122. package/dist/web/assets/{railroad-abnf-HS5TGJTU-DTxLo5az.js → railroad-abnf-HS5TGJTU-DfI0hiCR.js} +1 -1
  123. package/dist/web/assets/{railroad-ebnf-LZEXJU2U-BmP2IZGe.js → railroad-ebnf-LZEXJU2U-paFQAnqR.js} +1 -1
  124. package/dist/web/assets/{railroad-peg-WCYAUIDC-Do60gMTC.js → railroad-peg-WCYAUIDC-B-2Um3QI.js} +1 -1
  125. package/dist/web/assets/{railroadDiagram-O6MQD6OU-5I8Qk1dI.js → railroadDiagram-O6MQD6OU-WkZPN5MA.js} +1 -1
  126. package/dist/web/assets/{requirementDiagram-BXWQKSXE-CIIM8YmC.js → requirementDiagram-BXWQKSXE-BJULFhuR.js} +1 -1
  127. package/dist/web/assets/{ro-RO-JPDTUUEW-DR9yu2MV.js → ro-RO-JPDTUUEW-yKjTMizq.js} +1 -1
  128. package/dist/web/assets/{ru-RU-B4JR7IUQ-B9uIhZUd.js → ru-RU-B4JR7IUQ-DITGdMQL.js} +1 -1
  129. package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BdGmiiCQ.js → sankeyDiagram-P5KCCOFB-b_vfju3s.js} +1 -1
  130. package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-BeCmgCpG.js → sequenceDiagram-WJ2MYXX4-D5n2aEN6.js} +1 -1
  131. package/dist/web/assets/{si-LK-N5RQ5JYF-C7DYCsOB.js → si-LK-N5RQ5JYF-DaFciStR.js} +1 -1
  132. package/dist/web/assets/{sk-SK-C5VTKIMK-BbBBLLCs.js → sk-SK-C5VTKIMK-CpyCIFtD.js} +1 -1
  133. package/dist/web/assets/{sl-SI-NN7IZMDC-BIaW2Iuj.js → sl-SI-NN7IZMDC-cnwvs7FN.js} +1 -1
  134. package/dist/web/assets/{stateDiagram-D77RDMKH-CmENpSfH.js → stateDiagram-D77RDMKH-CbFjpKDm.js} +1 -1
  135. package/dist/web/assets/stateDiagram-v2-MP3YSRHH-MOkTt8Mv.js +1 -0
  136. package/dist/web/assets/subset-shared.chunk-CWAaXrHb.js +1 -0
  137. package/dist/web/assets/{subset-worker.chunk-Bi16Jh4x.js → subset-worker.chunk-Cebu2JDe.js} +1 -1
  138. package/dist/web/assets/{sv-SE-XGPEYMSR-D5A1U3nk.js → sv-SE-XGPEYMSR-dOntI1Fk.js} +1 -1
  139. package/dist/web/assets/{swimlanes-42K2YHIH-_ymwQg8o.js → swimlanes-42K2YHIH-DtmsuzRR.js} +1 -1
  140. package/dist/web/assets/swimlanesDiagram-VR7AAH4N-v2ue4raa.js +8 -0
  141. package/dist/web/assets/{ta-IN-2NMHFXQM-BDzXUwiE.js → ta-IN-2NMHFXQM-Comju6i2.js} +1 -1
  142. package/dist/web/assets/{th-TH-HPSO5L25-DdVnWuWW.js → th-TH-HPSO5L25-uPCBvbue.js} +1 -1
  143. package/dist/web/assets/{timeline-definition-24CTP7MA-CRcZde4k.js → timeline-definition-24CTP7MA-DKZOmZFD.js} +1 -1
  144. package/dist/web/assets/{tr-TR-DEFEU3FU-BQwfGN3o.js → tr-TR-DEFEU3FU-LaP2C4go.js} +1 -1
  145. package/dist/web/assets/{treeView-Q6P3EWNA-BHZZuSXr.js → treeView-Q6P3EWNA-DJii9IFr.js} +1 -1
  146. package/dist/web/assets/{treemap-WGGIJYW6-DAzotEQZ.js → treemap-WGGIJYW6-U42pUP2Q.js} +1 -1
  147. package/dist/web/assets/{uk-UA-QMV73CPH-CFYnWqHD.js → uk-UA-QMV73CPH-u-WBulor.js} +1 -1
  148. package/dist/web/assets/{vennDiagram-4TSXK5OY-BsNz9Qnn.js → vennDiagram-4TSXK5OY-C8_zJPdp.js} +1 -1
  149. package/dist/web/assets/{vi-VN-M7AON7JQ-DO3BySmm.js → vi-VN-M7AON7JQ-DOSv3aq5.js} +1 -1
  150. package/dist/web/assets/{wardley-WFR3VGLG-Cc2r8Vp8.js → wardley-WFR3VGLG-DU89dbbh.js} +1 -1
  151. package/dist/web/assets/{wardleyDiagram-VM6X3IG4-47UvJO4N.js → wardleyDiagram-VM6X3IG4-BGnASelu.js} +1 -1
  152. package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CVl63zr-.js → xychartDiagram-S5SC5T6Z-js8-abv7.js} +1 -1
  153. package/dist/web/assets/{zh-CN-LNUGB5OW-DKJ2CF-O.js → zh-CN-LNUGB5OW-B8soJHf3.js} +1 -1
  154. package/dist/web/assets/{zh-HK-E62DVLB3-DHW8OH5j.js → zh-HK-E62DVLB3-DKByBmSJ.js} +1 -1
  155. package/dist/web/assets/{zh-TW-RAJ6MFWO-B9cMDifZ.js → zh-TW-RAJ6MFWO-CYBStGL6.js} +1 -1
  156. package/dist/web/favicon-32.png +0 -0
  157. package/dist/web/favicon.svg +12 -0
  158. package/dist/web/index.html +6 -3
  159. package/package.json +5 -3
  160. package/dist/web/assets/channel-gFr9jEgP.js +0 -1
  161. package/dist/web/assets/classDiagram-ZZMXUADV-9J2CNLBI.js +0 -1
  162. package/dist/web/assets/classDiagram-v2-VYDZK3BY-9J2CNLBI.js +0 -1
  163. package/dist/web/assets/flowDiagram-HODETNUW--2QUGuZ7.js +0 -1
  164. package/dist/web/assets/index-B2hGCC2V.js +0 -212
  165. package/dist/web/assets/percentages-BXMCSKIN-Br7824d3.js +0 -1
  166. package/dist/web/assets/stateDiagram-v2-MP3YSRHH-BWgn52cG.js +0 -1
  167. package/dist/web/assets/subset-shared.chunk-d2GNuFHV.js +0 -1
  168. package/dist/web/assets/swimlanesDiagram-VR7AAH4N-fzrjtpfS.js +0 -8
@@ -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
- const COOKIE = "excaliagent_token";
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 === COOKIE)
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
- async function readJson(req) {
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 > MAX_BODY)
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
- async function api(req, res, pathname) {
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 UUID");
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": `${COOKIE}=${opts.token}; HttpOnly; SameSite=Strict; Path=/`,
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)
@@ -1,5 +1,10 @@
1
- /** Claude session ids are UUIDs; anything else is never put into a command. */
2
- export const SESSION_ID = /^[0-9a-f-]{36}$/i;
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: `claude --resume ${s.id}`,
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
- /** pid → pane for every foreground process of every pane; `panes` supplies ids and labels. */
54
- export function parsePaneMap(infos, panes = new Map()) {
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
- /** scene.json in stateDir, written atomically (temp file + fsync + rename), saves serialized. */
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
- async function write(scene) {
15
- await mkdir(stateDir, { recursive: true });
16
- const tmp = `${path}.tmp-${process.pid}-${seq++}`;
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(scene));
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, path);
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
- return { list, listDormant, get, watch };
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
  }