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
@@ -0,0 +1,144 @@
1
+ // Which OpenCode sessions are running, and in which process. OpenCode
2
+ // (verified on 1.19.3, Linux) keeps no session registry, lock or port file:
3
+ // the TUI is one process (the server runs in a worker thread, no listener
4
+ // unless --port is given) and the session it shows lives only in its memory.
5
+ // What is visible from outside: the TUI's argv (`-s/--session <id>` when it was
6
+ // started on a session) and its working directory (= the session's
7
+ // `directory` for sessions it creates). So:
8
+ //
9
+ // 1. a TUI started with `-s <id>` (not `--fork`) shows that session;
10
+ // 2. any other TUI shows the most recently updated root session of its
11
+ // directory, if that session was updated after the TUI started (a TUI on
12
+ // its home screen, or idle on an older session, claims nothing).
13
+ //
14
+ // This is a heuristic; docs/design.md "OpenCode data" lists where it is wrong.
15
+ // Only rule 1 is exact; only exact matches may be stopped from the board.
16
+ // Pure matching lives in `assignOpenCodeLive`; the /proc reading is Linux-only
17
+ // and takes a proc root, so tests read a synthetic tree.
18
+ import { readlink } from "node:fs/promises";
19
+ import { basename, join } from "node:path";
20
+ import { createProcessTable } from "../desktop/process-table.js";
21
+ import { bootTime, startedAt } from "./proc.js";
22
+ /** OpenCode session ids: `ses_` + 12 hex (time) + 14 base62. */
23
+ export const OPENCODE_ID = /^ses_[0-9a-zA-Z]{26}$/;
24
+ /** The native binary (`opencode`, nix `.opencode-wrapped`), or the npm shim (`node …/bin/opencode`). */
25
+ export function looksLikeOpenCode(argv, exe) {
26
+ const names = [argv?.[0], exe].filter((n) => !!n).map((n) => basename(n));
27
+ if (names.some((n) => /^\.?opencode(-wrapped)?(\.exe)?$/.test(n)))
28
+ return true;
29
+ const script = argv?.[1] ? basename(argv[1]) : "";
30
+ return names.some((n) => /^(node|bun)(\.exe)?$/.test(n)) && (script === "opencode" || script === "opencode.js");
31
+ }
32
+ const VALUE_FLAGS = new Set([
33
+ "--log-level", "--port", "--hostname", "--mdns-domain", "--cors", "-m", "--model", "-s", "--session", "--prompt",
34
+ "--agent", "--replay-limit",
35
+ ]);
36
+ // Subcommands that are not the interactive TUI (`opencode [project]`, `opencode pr <n>`).
37
+ const NOT_TUI = new Set([
38
+ "completion", "acp", "mcp", "attach", "run", "debug", "providers", "auth", "agent", "upgrade", "uninstall", "serve",
39
+ "web", "models", "stats", "export", "import", "github", "session", "plugin", "plug", "db",
40
+ ]);
41
+ function args(argv) {
42
+ const shim = /^(node|bun)(\.exe)?$/.test(basename(argv[0] ?? ""));
43
+ return argv.slice(shim ? 2 : 1);
44
+ }
45
+ /** First positional word: a subcommand, a project dir, or undefined. */
46
+ export function openCodeSubcommand(argv) {
47
+ const a = args(argv);
48
+ for (let i = 0; i < a.length; i++) {
49
+ const w = a[i];
50
+ if (w === "--")
51
+ return a[i + 1];
52
+ if (w.startsWith("-")) {
53
+ if (VALUE_FLAGS.has(w))
54
+ i++;
55
+ continue;
56
+ }
57
+ return w;
58
+ }
59
+ return undefined;
60
+ }
61
+ export const isOpenCodeTui = (argv) => {
62
+ const sub = openCodeSubcommand(argv);
63
+ return sub === undefined || !NOT_TUI.has(sub);
64
+ };
65
+ /** `-s <id>`, `--session <id>`, `--session=<id>`; undefined with `--fork` (that makes a new session). */
66
+ export function argvSessionId(argv) {
67
+ const a = args(argv);
68
+ if (a.includes("--fork"))
69
+ return undefined;
70
+ for (let i = 0; i < a.length; i++) {
71
+ const w = a[i];
72
+ const v = w === "-s" || w === "--session" ? a[i + 1] : w.startsWith("--session=") ? w.slice(10) : undefined;
73
+ if (v && OPENCODE_ID.test(v))
74
+ return v;
75
+ }
76
+ return undefined;
77
+ }
78
+ /**
79
+ * Pure. `byId` finds a root, unarchived session by id; `inDir` lists the root,
80
+ * unarchived sessions whose directory is `dir`.
81
+ */
82
+ export function assignOpenCodeLive(procs, byId, inDir) {
83
+ const pids = new Set(procs.map((p) => p.pid));
84
+ // The npm shim and its native child count once, as the shim (the pane's foreground).
85
+ // Newest first: a later TUI is more likely on the newest session of a directory.
86
+ const tuis = procs
87
+ .filter((p) => isOpenCodeTui(p.argv) && !pids.has(p.ppid))
88
+ .sort((a, b) => b.startedAt - a.startedAt || b.pid - a.pid);
89
+ const out = [];
90
+ const taken = new Set();
91
+ const rest = [];
92
+ for (const t of tuis) {
93
+ const id = argvSessionId(t.argv);
94
+ // A TUI that names its session never guesses another one.
95
+ if (!id) {
96
+ rest.push(t);
97
+ continue;
98
+ }
99
+ const s = byId(id);
100
+ if (s && !taken.has(s.id)) {
101
+ taken.add(s.id);
102
+ out.push({ id: s.id, pid: t.pid, since: t.startedAt, exact: true });
103
+ }
104
+ }
105
+ for (const t of rest) {
106
+ if (!t.cwd)
107
+ continue;
108
+ const best = inDir(t.cwd)
109
+ .filter((s) => !taken.has(s.id) && s.updatedAt >= t.startedAt)
110
+ .sort((a, b) => b.updatedAt - a.updatedAt)[0];
111
+ if (!best)
112
+ continue;
113
+ taken.add(best.id);
114
+ out.push({ id: best.id, pid: t.pid, since: t.startedAt, exact: false });
115
+ }
116
+ return out;
117
+ }
118
+ /**
119
+ * Running OpenCode processes with argv, cwd and start time, read from
120
+ * `procRoot`. Undefined when it has no boot time (not Linux): liveness unknown.
121
+ */
122
+ export async function scanOpenCodeProcesses(procRoot = "/proc") {
123
+ const btime = await bootTime(procRoot);
124
+ if (btime === undefined)
125
+ return undefined;
126
+ const table = createProcessTable({ platform: "linux", procRoot });
127
+ const out = [];
128
+ for (const p of await table.list()) {
129
+ // Cheap filter before reading argv: the binary, the nix wrapper, or a shim.
130
+ if (!/opencode|node|bun|MainThread/i.test(p.comm))
131
+ continue;
132
+ const [argv, exe] = await Promise.all([table.argv(p.pid), table.exe(p.pid)]);
133
+ if (!argv || !looksLikeOpenCode(argv, exe))
134
+ continue;
135
+ const [cwd, started] = await Promise.all([
136
+ readlink(join(procRoot, String(p.pid), "cwd")).catch(() => undefined),
137
+ startedAt(procRoot, p.pid, btime),
138
+ ]);
139
+ if (!started)
140
+ continue;
141
+ out.push({ pid: p.pid, ppid: p.ppid, argv, ...(cwd ? { cwd } : {}), startedAt: started });
142
+ }
143
+ return out;
144
+ }
@@ -0,0 +1,265 @@
1
+ // OpenCode source: sessions from OpenCode's SQLite database
2
+ // ($XDG_DATA_HOME/opencode/opencode.db), read-only through `node:sqlite`
3
+ // (Node 22.13+; without it the source logs once and shows nothing). Live
4
+ // sessions come from the running TUI processes (opencode-live.ts); status from
5
+ // the newest message since the TUI started: busy while a reply is unfinished.
6
+ // Permission prompts are not stored, so they show as busy.
7
+ // Formats: docs/design.md "OpenCode data".
8
+ import { stat } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { basename, join } from "node:path";
11
+ import { createProcessTable } from "../desktop/process-table.js";
12
+ import { GitReader } from "./git.js";
13
+ import { onceLogger } from "./log.js";
14
+ import { assignOpenCodeLive, isOpenCodeTui, looksLikeOpenCode, OPENCODE_ID, scanOpenCodeProcesses, } from "./opencode-live.js";
15
+ export const openCodeResumeCommand = (id) => `opencode -s ${id}`;
16
+ export function defaultOpenCodeDataDir(env = process.env) {
17
+ return join(env.XDG_DATA_HOME || join(homedir(), ".local", "share"), "opencode");
18
+ }
19
+ export function defaultOpenCodeDbPath(dataDir, env = process.env) {
20
+ const override = env.OPENCODE_DB;
21
+ if (override && override !== ":memory:")
22
+ return override.startsWith("/") ? override : join(dataDir, override);
23
+ return join(dataDir, "opencode.db");
24
+ }
25
+ /** Default titles OpenCode gives before it has generated one. */
26
+ export function isDefaultTitle(title) {
27
+ return /^(New session|Child session) - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(title);
28
+ }
29
+ const COLS = "id, directory, title, time_updated, time_created, parent_id, time_archived";
30
+ const ROOT = "parent_id IS NULL AND time_archived IS NULL";
31
+ /** node:sqlite, read-only. */
32
+ export async function openOpenCodeDb(path) {
33
+ if (!(await stat(path).catch(() => undefined)))
34
+ return undefined;
35
+ const { DatabaseSync } = await import("node:sqlite");
36
+ const db = new DatabaseSync(path, { readOnly: true });
37
+ // OpenCode writes all the time (WAL); wait briefly instead of failing a poll.
38
+ db.exec("PRAGMA busy_timeout = 250");
39
+ const placeholders = (n) => Array(n).fill("?").join(",");
40
+ return {
41
+ recent: (limit) => db.prepare(`SELECT ${COLS} FROM session WHERE ${ROOT} ORDER BY time_updated DESC LIMIT ?`).all(limit),
42
+ byIds: (ids) => ids.length ? db.prepare(`SELECT ${COLS} FROM session WHERE id IN (${placeholders(ids.length)})`).all(...ids) : [],
43
+ inDirs: (dirs) => dirs.length
44
+ ? db.prepare(`SELECT ${COLS} FROM session WHERE ${ROOT} AND directory IN (${placeholders(dirs.length)})`).all(...dirs)
45
+ : [],
46
+ lastMessage: (id) => db
47
+ .prepare(`SELECT json_extract(data, '$.role') AS role, json_extract(data, '$.time.completed') AS completed, time_created AS created
48
+ FROM message WHERE session_id = ? ORDER BY time_created DESC, id DESC LIMIT 1`)
49
+ .get(id),
50
+ close: () => db.close(),
51
+ };
52
+ }
53
+ function defaultIsOpenCodeTui(procRoot) {
54
+ const table = procRoot ? createProcessTable({ platform: "linux", procRoot }) : createProcessTable();
55
+ return async (pid) => {
56
+ const argv = await table.argv(pid);
57
+ return !!argv && looksLikeOpenCode(argv, await table.exe(pid)) && isOpenCodeTui(argv);
58
+ };
59
+ }
60
+ /** Busy while the newest message since the TUI started is a prompt or an unfinished reply. */
61
+ export function openCodeStatus(last, since) {
62
+ if (!last || last.created < since)
63
+ return "idle";
64
+ if (last.role === "user")
65
+ return "busy";
66
+ return last.role === "assistant" && last.completed == null ? "busy" : "idle";
67
+ }
68
+ const epochMs = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0);
69
+ export function openCodeSession(row, opts) {
70
+ const s = {
71
+ id: row.id,
72
+ source: "opencode",
73
+ cwd: row.directory,
74
+ repo: basename(row.directory),
75
+ status: opts.live ? (opts.status ?? "idle") : "dormant",
76
+ // A missing or odd value never becomes a number the board would date from 1970 (it shows no age for 0).
77
+ lastActivityAt: epochMs(row.time_updated) || epochMs(row.time_created),
78
+ cacheTtlMs: 0,
79
+ cacheExpiresAt: 0,
80
+ live: !!opts.live,
81
+ prLinks: [],
82
+ resumeCommand: openCodeResumeCommand(row.id),
83
+ };
84
+ if (opts.live)
85
+ s.pid = opts.live.pid;
86
+ // Stop only for a TUI started on exactly this session, never a directory guess.
87
+ if (opts.live?.exact)
88
+ s.canEnd = true;
89
+ if (row.title && !isDefaultTitle(row.title))
90
+ s.title = row.title;
91
+ if (opts.git) {
92
+ s.repo = basename(opts.git.root);
93
+ s.repoRoot = opts.git.root;
94
+ s.branch = opts.git.branch;
95
+ }
96
+ return s;
97
+ }
98
+ const candidate = (r) => ({ id: r.id, directory: r.directory, updatedAt: r.time_updated });
99
+ /** Rows whose id is not a valid OpenCode id never become a session (or a command). */
100
+ const valid = (rows) => rows.filter((r) => typeof r.id === "string" && OPENCODE_ID.test(r.id));
101
+ export function createOpenCodeSource(opts = {}) {
102
+ const dataDir = opts.dataDir ?? defaultOpenCodeDataDir();
103
+ const dbPath = opts.dbPath ?? defaultOpenCodeDbPath(dataDir);
104
+ const openDb = opts.openDb ?? openOpenCodeDb;
105
+ const scan = opts.scan ?? (() => scanOpenCodeProcesses(opts.procRoot));
106
+ const pollMs = opts.pollMs ?? 2000;
107
+ const cacheMs = opts.cacheMs ?? pollMs;
108
+ const maxDormant = opts.maxDormant ?? 200;
109
+ const warn = onceLogger(opts.log ?? console.warn);
110
+ const git = new GitReader({ ttlMs: opts.gitTtlMs ?? 2000 });
111
+ const isTui = opts.isOpenCodeTui ?? defaultIsOpenCodeTui(opts.procRoot);
112
+ const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
113
+ let db;
114
+ let opening;
115
+ async function database() {
116
+ if (db)
117
+ return db;
118
+ opening ??= openDb(dbPath)
119
+ .catch((e) => {
120
+ warn(`excaliagent: cannot open the OpenCode database ${dbPath}: ${e.message}`, "opencode-db");
121
+ return undefined;
122
+ })
123
+ .finally(() => (opening = undefined));
124
+ db = await opening;
125
+ return db;
126
+ }
127
+ /** Runs a query; on failure (schema change, file replaced) reopens next time. */
128
+ async function query(fn, fallback) {
129
+ const d = await database();
130
+ if (!d)
131
+ return fallback;
132
+ try {
133
+ return fn(d);
134
+ }
135
+ catch (e) {
136
+ warn(`excaliagent: reading the OpenCode database failed: ${e.message}`, "opencode-query");
137
+ db = undefined;
138
+ try {
139
+ d.close();
140
+ }
141
+ catch {
142
+ // already closed
143
+ }
144
+ return fallback;
145
+ }
146
+ }
147
+ async function live() {
148
+ const procs = await scan().catch(() => undefined);
149
+ if (!procs?.length)
150
+ return [];
151
+ const ids = procs.map((p) => p.argv).flatMap((a) => a.filter((w) => OPENCODE_ID.test(w) || w.startsWith("--session=")).map((w) => w.replace(/^--session=/, "")));
152
+ const dirs = [...new Set(procs.map((p) => p.cwd).filter((c) => !!c))];
153
+ const rows = new Map();
154
+ for (const r of valid(await query((d) => [...d.byIds(ids), ...d.inDirs(dirs)], [])))
155
+ rows.set(r.id, r);
156
+ const usable = [...rows.values()].filter((r) => r.parent_id == null && r.time_archived == null);
157
+ const assigned = assignOpenCodeLive(procs, (id) => {
158
+ const r = rows.get(id);
159
+ return r && usable.includes(r) ? candidate(r) : undefined;
160
+ }, (dir) => usable.filter((r) => r.directory === dir).map(candidate));
161
+ return assigned.map((l) => ({ row: rows.get(l.id), live: l }));
162
+ }
163
+ let look;
164
+ let looking;
165
+ async function current(fresh = false) {
166
+ if (!fresh && look && Date.now() - look.at < cacheMs)
167
+ return look;
168
+ looking ??= (async () => {
169
+ const found = await live();
170
+ look = { at: Date.now(), live: found, sessions: await build(found) };
171
+ return look;
172
+ })().finally(() => (looking = undefined));
173
+ return looking;
174
+ }
175
+ async function list() {
176
+ return (await current()).sessions;
177
+ }
178
+ async function build(found) {
179
+ const out = [];
180
+ for (const { row, live: l } of found) {
181
+ const last = await query((d) => d.lastMessage(row.id), undefined);
182
+ out.push(openCodeSession(row, { live: l, status: openCodeStatus(last, l.since), git: await git.get(row.directory) }));
183
+ }
184
+ return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
185
+ }
186
+ async function listDormant() {
187
+ const liveIds = new Set((await current()).live.map((l) => l.row.id));
188
+ const rows = valid(await query((d) => d.recent(maxDormant + liveIds.size), [])).filter((r) => !liveIds.has(r.id));
189
+ const out = [];
190
+ for (const r of rows.slice(0, maxDormant))
191
+ out.push(openCodeSession(r, { git: await git.get(r.directory) }));
192
+ return out;
193
+ }
194
+ async function get(id) {
195
+ if (!OPENCODE_ID.test(id))
196
+ return undefined;
197
+ const running = (await list()).find((s) => s.id === id);
198
+ if (running)
199
+ return running;
200
+ const [row] = valid(await query((d) => d.byIds([id]), []));
201
+ return row ? openCodeSession(row, { git: await git.get(row.directory) }) : undefined;
202
+ }
203
+ function watch(cb) {
204
+ let closed = false;
205
+ let running = false;
206
+ let last;
207
+ // No file to watch tells more than a poll: the db changes on every token.
208
+ const refresh = async () => {
209
+ if (running || closed)
210
+ return;
211
+ running = true;
212
+ try {
213
+ const sessions = (await current(true)).sessions;
214
+ const key = JSON.stringify(sessions);
215
+ if (!closed && key !== last) {
216
+ last = key;
217
+ try {
218
+ cb(sessions);
219
+ }
220
+ catch (e) {
221
+ warn(`excaliagent: session watcher callback threw: ${e.message}`, "opencode-watch-callback");
222
+ }
223
+ }
224
+ }
225
+ catch (e) {
226
+ warn(`excaliagent: refreshing OpenCode sessions failed: ${e.message}`, "opencode-refresh");
227
+ }
228
+ finally {
229
+ running = false;
230
+ }
231
+ };
232
+ const poll = setInterval(() => void refresh(), pollMs);
233
+ void refresh();
234
+ return () => {
235
+ closed = true;
236
+ clearInterval(poll);
237
+ db?.close();
238
+ db = undefined;
239
+ };
240
+ }
241
+ /**
242
+ * SIGTERM to the TUI: OpenCode closes the reply, restores the terminal and
243
+ * exits. Only for a TUI started with `-s <id>`: a session matched by
244
+ * directory is "unsupported", so a guess never ends a process.
245
+ */
246
+ async function end(id) {
247
+ if (!OPENCODE_ID.test(id))
248
+ return "not-live";
249
+ const l = (await current()).live.find((x) => x.row.id === id);
250
+ if (!l)
251
+ return "not-live";
252
+ if (!l.live.exact)
253
+ return "unsupported";
254
+ if (!(await isTui(l.live.pid).catch(() => false)))
255
+ return "not-live";
256
+ try {
257
+ kill(l.live.pid, "SIGTERM");
258
+ }
259
+ catch {
260
+ return "not-live";
261
+ }
262
+ return "ended";
263
+ }
264
+ return { list, listDormant, get, watch, end };
265
+ }
@@ -0,0 +1,34 @@
1
+ // Small /proc helpers shared by the adapters that find their sessions among
2
+ // running processes (Codex, OpenCode). Every reader takes the proc root, so
3
+ // tests and the demo point it at a synthetic tree (EXCALIAGENT_PROC_ROOT) and
4
+ // never read the machine's real processes.
5
+ import { readFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ /** Where the server reads processes from: the real /proc unless overridden (tests, demo). */
8
+ export function procRootFromEnv(env = process.env) {
9
+ return env.EXCALIAGENT_PROC_ROOT || undefined;
10
+ }
11
+ /** Field 22 of /proc/<pid>/stat: start time in clock ticks since boot. */
12
+ export function parseStartTicks(stat) {
13
+ const close = stat.lastIndexOf(")");
14
+ const ticks = Number(stat.slice(close + 1).trim().split(/\s+/)[19]);
15
+ return close > 0 && Number.isFinite(ticks) ? ticks : undefined;
16
+ }
17
+ /** Linux clock ticks per second (USER_HZ); 100 on every mainstream architecture. */
18
+ export const USER_HZ = 100;
19
+ /** Boot time (epoch s) from <procRoot>/stat, or undefined (not Linux). */
20
+ export async function bootTime(procRoot) {
21
+ const stat = await readFile(join(procRoot, "stat"), "utf8").catch(() => undefined);
22
+ const btime = stat ? Number(/^btime (\d+)$/m.exec(stat)?.[1]) : NaN;
23
+ return Number.isFinite(btime) && btime > 0 ? btime : undefined;
24
+ }
25
+ /**
26
+ * Epoch ms a process started (btime is whole seconds, so this can be up to 1 s
27
+ * early), or 0 when unknown.
28
+ */
29
+ export async function startedAt(procRoot, pid, btime) {
30
+ if (btime === undefined)
31
+ return 0;
32
+ const ticks = parseStartTicks(await readFile(join(procRoot, String(pid), "stat"), "utf8").catch(() => ""));
33
+ return ticks === undefined ? 0 : Math.floor((btime + ticks / USER_HZ) * 1000);
34
+ }
@@ -1,12 +1,9 @@
1
1
  // Incremental scanner for Claude Code transcripts
2
2
  // (<claudeDir>/projects/<slug>/<sessionId>.jsonl). Keeps a byte offset per file
3
3
  // and parses only appended bytes. Never keeps message content.
4
- import { open, stat } from "node:fs/promises";
5
- import { onceLogger } from "./log.js";
4
+ import { JsonlScanner } from "./jsonl.js";
6
5
  export const TTL_1H = 3600_000;
7
6
  export const TTL_5M = 300_000;
8
- const CHUNK = 1 << 20;
9
- const NL = 0x0a;
10
7
  // Cheap prefilter; the parsed top-level `type` is still checked.
11
8
  const INTERESTING = /"type":"(?:ai-title|agent-name|pr-link|continued-in|user|assistant|api-request)"/;
12
9
  const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
@@ -100,105 +97,14 @@ function snapshot(acc) {
100
97
  out.gitBranch = acc.gitBranch;
101
98
  return out;
102
99
  }
103
- export class TranscriptScanner {
104
- files = new Map();
105
- inflight = new Map();
106
- warn;
100
+ const fold = {
101
+ empty: emptyAcc,
102
+ interesting: (line) => INTERESTING.test(line),
103
+ apply: applyLine,
104
+ snapshot,
105
+ };
106
+ export class TranscriptScanner extends JsonlScanner {
107
107
  constructor(opts = {}) {
108
- this.warn = opts.warn ?? onceLogger(opts.log ?? console.warn);
109
- }
110
- /** Scans new bytes of `path` and returns the folded info, or undefined if the file is gone. */
111
- scan(path) {
112
- const running = this.inflight.get(path);
113
- if (running)
114
- return running;
115
- const p = this.doScan(path).finally(() => this.inflight.delete(path));
116
- this.inflight.set(path, p);
117
- return p;
118
- }
119
- /** Read bookkeeping, for tests. */
120
- stats(path) {
121
- const s = this.files.get(path);
122
- return s && { offset: s.offset, bytesRead: s.bytesRead };
123
- }
124
- forget(path) {
125
- this.files.delete(path);
126
- }
127
- async doScan(path) {
128
- let st;
129
- try {
130
- st = await stat(path);
131
- }
132
- catch {
133
- this.files.delete(path);
134
- return undefined;
135
- }
136
- let s = this.files.get(path);
137
- const replaced = !s || s.ino !== st.ino || s.dev !== st.dev;
138
- const shrunk = !!s && st.size < s.offset;
139
- // Same size but touched: rewritten in place (appends always grow the file).
140
- const rewritten = !!s && st.size === s.offset && st.mtimeMs !== s.mtimeMs;
141
- if (!s || replaced || shrunk || rewritten) {
142
- s = { ino: st.ino, dev: st.dev, mtimeMs: st.mtimeMs, offset: 0, bytesRead: s?.bytesRead ?? 0, partial: Buffer.alloc(0), acc: emptyAcc() };
143
- this.files.set(path, s);
144
- }
145
- if (st.size > s.offset) {
146
- try {
147
- await this.readFrom(path, s, st.size);
148
- }
149
- catch (e) {
150
- this.warn(`excaliagent: cannot read transcript ${path}: ${e.message}`, path);
151
- }
152
- }
153
- s.mtimeMs = st.mtimeMs;
154
- return snapshot(s.acc);
155
- }
156
- async readFrom(path, s, end) {
157
- const fh = await open(path, "r");
158
- try {
159
- const buf = Buffer.allocUnsafe(Math.min(CHUNK, end - s.offset));
160
- while (s.offset < end) {
161
- const { bytesRead } = await fh.read(buf, 0, Math.min(buf.length, end - s.offset), s.offset);
162
- if (bytesRead === 0)
163
- break;
164
- s.offset += bytesRead;
165
- s.bytesRead += bytesRead;
166
- this.consume(path, s, buf.subarray(0, bytesRead));
167
- }
168
- }
169
- finally {
170
- await fh.close();
171
- }
172
- }
173
- consume(path, s, chunk) {
174
- let start = 0;
175
- let nl = chunk.indexOf(NL, start);
176
- while (nl !== -1) {
177
- const piece = chunk.subarray(start, nl);
178
- const line = s.partial.length ? Buffer.concat([s.partial, piece]) : piece;
179
- s.partial = Buffer.alloc(0);
180
- this.parseLine(path, s.acc, line);
181
- start = nl + 1;
182
- nl = chunk.indexOf(NL, start);
183
- }
184
- if (start < chunk.length)
185
- s.partial = Buffer.concat([s.partial, chunk.subarray(start)]);
186
- }
187
- parseLine(path, acc, line) {
188
- if (line.length === 0)
189
- return;
190
- const text = line.toString("utf8");
191
- if (!INTERESTING.test(text))
192
- return;
193
- let o;
194
- try {
195
- o = JSON.parse(text);
196
- }
197
- catch {
198
- this.warn(`excaliagent: skipping malformed line(s) in ${path}`, path);
199
- return;
200
- }
201
- if (o && typeof o === "object" && !Array.isArray(o))
202
- applyLine(acc, o);
108
+ super(fold, { ...opts, noun: "transcript" });
203
109
  }
204
110
  }
@@ -0,0 +1,23 @@
1
+ // The access token: created once per state dir and reused, so the browser's
2
+ // cookie (and a bookmark of the board) survives restarts. The file is 0600;
3
+ // whatever can read it could read ~/.claude too.
4
+ import { randomBytes } from "node:crypto";
5
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ const VALID = /^[A-Za-z0-9_-]{32,}$/;
8
+ export async function loadOrCreateToken(stateDir) {
9
+ const path = join(stateDir, "token");
10
+ try {
11
+ const t = (await readFile(path, "utf8")).trim();
12
+ if (VALID.test(t))
13
+ return t;
14
+ }
15
+ catch {
16
+ // missing: create it
17
+ }
18
+ await mkdir(stateDir, { recursive: true });
19
+ const token = randomBytes(24).toString("base64url");
20
+ await writeFile(path, `${token}\n`, { mode: 0o600 });
21
+ await chmod(path, 0o600); // mode only applies when the file is created
22
+ return token;
23
+ }
@@ -0,0 +1,62 @@
1
+ // Character packs (docs/design.md "Characters"): shared by the server (which
2
+ // lists, validates and stores them) and the board (which assigns and draws them).
3
+ //
4
+ // A pack is an ordered list of characters. Built-in packs are Unicode emoji,
5
+ // drawn by the system's emoji font; their ids start with "@", a character that
6
+ // custom pack names (folder names under <stateDir>/packs/) cannot contain, so
7
+ // the two never collide. Custom packs hold emoji and/or image files.
8
+ export const DEFAULT_PACK = "@animals";
9
+ /** Custom pack names: a folder name that can never escape `<stateDir>/packs/`. */
10
+ export const PACK_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
11
+ /** Characters per pack beyond this are ignored (and refused on add). */
12
+ export const MAX_PACK_CHARACTERS = 200;
13
+ /** `excaliagent-emoji-1f431`: the file id encodes the emoji, so the board can redraw it from the id alone. */
14
+ export function emojiFileId(emoji) {
15
+ return `excaliagent-emoji-${[...emoji].map((c) => c.codePointAt(0).toString(16)).join("-")}`;
16
+ }
17
+ /** The emoji an `emojiFileId` encodes, or undefined. */
18
+ export function emojiFromFileId(fileId) {
19
+ const m = /^excaliagent-emoji-([0-9a-f]{1,6}(?:-[0-9a-f]{1,6}){0,15})$/.exec(fileId);
20
+ if (!m)
21
+ return undefined;
22
+ try {
23
+ const emoji = String.fromCodePoint(...m[1].split("-").map((h) => parseInt(h, 16)));
24
+ return validEmoji(emoji) ? emoji : undefined;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
31
+ /**
32
+ * One emoji: a single grapheme (ZWJ sequences, skin tones and flags included),
33
+ * at most 32 UTF-16 units, with a pictographic or regional-indicator character
34
+ * and no control characters or whitespace.
35
+ */
36
+ export function validEmoji(s) {
37
+ if (typeof s !== "string" || s.length === 0 || s.length > 32)
38
+ return false;
39
+ if (/[\p{Cc}\s]/u.test(s))
40
+ return false;
41
+ if (!/\p{Extended_Pictographic}|\p{Regional_Indicator}/u.test(s))
42
+ return false;
43
+ return [...segmenter.segment(s)].length === 1;
44
+ }
45
+ const emojiPack = (id, name, emoji) => ({
46
+ id,
47
+ name,
48
+ builtIn: true,
49
+ characters: emoji
50
+ .split(" ")
51
+ .filter(Boolean)
52
+ .map((e) => ({ id: e, kind: "emoji", fileId: emojiFileId(e) })),
53
+ });
54
+ // Emoji from Unicode ≤ 12 without ZWJ sequences or variation selectors, so
55
+ // every common emoji font (Noto Color Emoji, Apple, Segoe UI) has them.
56
+ export const BUILT_IN_PACKS = [
57
+ emojiPack("@animals", "Animals", "🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐨 🐯 🦁 🐮 🐷 🐸 🐵 🐔 🐧 🦆 🦅 🦉 🐺 🐗 🐴 🦄 🐝 🐞 🦋 🐌 🐢 🐍 🦎 🦒 🦓 🐘 🦔 🦘"),
58
+ emojiPack("@cats", "Cats & friends", "😺 😸 😹 😻 😼 😽 🙀 😿 😾 🐱 🐈 🐯 🐅 🐆 🦁 🐶 🐕 🐩 🐹 🐰 🐇 🐭 🐁 🦔 🐤 🐥 🐣 🐦"),
59
+ emojiPack("@sea", "Sea", "🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐢 🐚 🦦 🐧 🦢 🦆 🦪 ⛵ 🚤 🛶 🚢 ⚓ 🏄 🌊 🧜 🌴"),
60
+ emojiPack("@fantasy", "Fantasy", "🐉 🐲 🦄 🧙 🧚 🧛 🧜 🧝 🧞 🧟 🦸 🦹 👻 👽 👾 🤖 👹 👺 💀 🎃 🦖 🦕 🔮 🗿 🏰 🛸 🌋 🌈 🍄"),
61
+ emojiPack("@food", "Food", "🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍒 🍑 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🌽 🥕 🥐 🥖 🥨 🧀 🥚 🥞 🥓 🍔 🍟 🍕 🌭 🥪 🌮 🌯 🍣 🍜 🍩 🍪 🍰 🍫 🍿"),
62
+ ];
Binary file
@@ -1 +1 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as t}from"./src-BWF7jMDv.js";import"./chunk-DU6HZSFF-DYHa8ENH.js";import"./dist-D-3cz42D.js";import{n,r,t as i}from"./chunk-SVP7TREG-CaeKcCSu.js";import{t as a}from"./chunk-JWPE2WC7-DVXcaiue.js";import{E as o,t as s}from"./mermaid-parser.core-Da_XEVmR.js";var c=o().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{a(e,i),e.title&&i.setTitle(e.title),e.rules.map(e=>i.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{i.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new s(n);let r=n.value;t.debug(`[ABNF Parser] Parsed rules:`,r.rules.length),h(r),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:i}},db:i,renderer:r,styles:n};export{g as diagram};
1
+ import{n as e}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as t}from"./src-BWF7jMDv.js";import"./chunk-DU6HZSFF-SqE0xdu5.js";import"./dist-DcwmvBdV.js";import{n,r,t as i}from"./chunk-SVP7TREG-DkRJF9jr.js";import{t as a}from"./chunk-JWPE2WC7-DVXcaiue.js";import{E as o,t as s}from"./mermaid-parser.core-Dbqkvrw-.js";var c=o().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{a(e,i),e.title&&i.setTitle(e.title),e.rules.map(e=>i.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{i.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new s(n);let r=n.value;t.debug(`[ABNF Parser] Parsed rules:`,r.rules.length),h(r),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:i}},db:i,renderer:r,styles:n};export{g as diagram};