mulmoterminal 4.1.1 → 4.2.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 (210) hide show
  1. package/README.md +27 -16
  2. package/common/byCodeUnit.ts +11 -0
  3. package/common/codeBlocks.ts +9 -5
  4. package/common/gitlabHosts.ts +60 -0
  5. package/common/isRecord.ts +6 -0
  6. package/common/isUnknownArray.ts +8 -0
  7. package/common/issueStartPlan.ts +18 -11
  8. package/common/keymap.ts +2 -0
  9. package/common/median.ts +7 -2
  10. package/common/prPhase.ts +3 -3
  11. package/common/readString.ts +28 -0
  12. package/common/repoEntry.ts +3 -2
  13. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CuUAI5MR.js → abnfDiagram-VRR7QNED-RjiYivmv-Ko4XgYrQ.js} +1 -1
  14. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-C_A3J6UT.js +1 -0
  15. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-rRDT09Vx.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DXXAwU3I.js} +1 -1
  16. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-By0PkzIO.js → blockDiagram-677ZJIJ3-BPuAJQRW-CKecg_L3.js} +1 -1
  17. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-GgOhwf63.js → c4Diagram-LMCZKHZV-C0LAqQso-5vO8udpQ.js} +1 -1
  18. package/dist/assets/channel-7wUqSdoX-CYxVMjfv.js +1 -0
  19. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-C_AxGcxt.js → chunk-32BRIVSS-BRrYpgtb-G2dVX9R4.js} +1 -1
  20. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DNopp04b.js → chunk-52WLFC77-BJ-ss3Xr-5jRfBwS-.js} +1 -1
  21. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL---6fNwPJ.js → chunk-C7G6YPKG-BZEucKEL-dH1xp00-.js} +1 -1
  22. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D5lN82UI.js → chunk-EX3LRPZG-DLS6FBN1-Bm_0Dq7o.js} +1 -1
  23. package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-DKxptXUj.js → chunk-FWX5IMBZ-DHLSFw1H-DMe5J-nM.js} +2 -2
  24. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-CAh9s_YU.js → chunk-HOUHSVGY-Bhlt8hXJ-ByrE_Y0q.js} +1 -1
  25. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-jwQc10EC.js → chunk-ICXQ74PX-DwgHBX_g-C7WGmPMk.js} +1 -1
  26. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js → chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js} +1 -1
  27. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BuRF4rGR.js → chunk-OGEWGWER-8Qy4a8b5-DJR11LQX.js} +1 -1
  28. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-BYgad2gt.js → chunk-PUDLZKDR-DcrWQRYh-CRXRg8rj.js} +1 -1
  29. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-DhnrB6RT.js → chunk-Q4XR5HBZ-ZXVGkG8Z-CuyNeF8W.js} +1 -1
  30. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Ur1KzthC.js → chunk-V7JOEXUC-DmGdheTX-BXL1l6dM.js} +1 -1
  31. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js → chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js} +1 -1
  32. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-2iUlb5mQ.js → chunk-VR4S4FIN-C6a91eNY-DcR5TXVW.js} +1 -1
  33. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js → chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js} +1 -1
  34. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Ct0mEJmO.js → chunk-ZGVPDNZ5-BYwxNFTK-DPJ63Ug-.js} +1 -1
  35. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-D9PYh-Ds.js +1 -0
  36. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-D9PYh-Ds.js +1 -0
  37. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BOGZ0d_V.js +1 -0
  38. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--BISNJm8X.js → cynefinDiagram-TSTJHNR4-DHA9iPo--C-vvAO00.js} +1 -1
  39. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-B459NNxB.js → dagre-VKFMJZFB--oJKqXBZ-j_nnCAk3.js} +1 -1
  40. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DVyaiIsO.js → diagram-FQU43EPY-CcJCB9bG-Bg8kALAQ.js} +1 -1
  41. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-BhuGPhW4.js → diagram-G47NLZAW-C_o-WGG1-D8IYmNOo.js} +1 -1
  42. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DzbRZHC1.js → diagram-NH7WQ7WH-CXJCYvY--ntIh1pVU.js} +1 -1
  43. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-D8t4UJW_.js → diagram-OA4YK3LP-BMzeJ87A-1rD8ncO1.js} +1 -1
  44. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-Bbl2-CHc.js → diagram-WEI45ONY-D_93NKqo-DWStdu4t.js} +1 -1
  45. package/dist/assets/{dist-ClO6UEBJ.js → dist-C1t_ZgKo.js} +1 -1
  46. package/dist/assets/{dist-D8AEcEHx.js → dist-CpuWpSIw.js} +1 -1
  47. package/dist/assets/{dist-Daghl_nN.js → dist-QEbKPDTX.js} +1 -1
  48. package/dist/assets/{dist-epQ42VP2.js → dist-w2lrwfZt.js} +1 -1
  49. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-Bbg4X-2o.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-uJbtiAJU.js} +1 -1
  50. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CKq_tipK.js → erDiagram-Q63AITRT-CiABnA0s-C2GoR6NA.js} +1 -1
  51. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-iI1MrDgd.js +1 -0
  52. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-CrOlHoXQ.js +1 -0
  53. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-jmx958Wo.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BM70MIdN.js} +1 -1
  54. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DlP31iSf.js +1 -0
  55. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-DuZaMN46.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-B_Pgy8IF.js} +1 -1
  56. package/dist/assets/index-C1urCATU.css +1 -0
  57. package/dist/assets/index-DIMX-Lgi.js +614 -0
  58. package/dist/assets/info-DKCQHKI2-Dplx5kMp-3WlBQbJQ.js +1 -0
  59. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-C8ikfN1o.js → infoDiagram-FWYZ7A6U-7UnoB5AP-Llrz1ikt.js} +1 -1
  60. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-ChrD-r9s.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-DkyrG0F4.js} +1 -1
  61. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-uY7L1btR.js → journeyDiagram-5HDEW3XC-c5xIah9o-C6nobV_J.js} +1 -1
  62. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CNv5ziZo.js → kanban-definition-HUTT4EX6-Cnt6loYD-r8kRECnn.js} +1 -1
  63. package/dist/assets/{lib-dCbEkBG7.js → lib-CGMPNazU.js} +1 -1
  64. package/dist/assets/{line-D7ziSjKi-BEYdEqj_.js → line-D7ziSjKi-BGdO3L7C.js} +1 -1
  65. package/dist/assets/{marp-CU_bIZEZ.js → marp-CYclI9Sp.js} +1 -1
  66. package/dist/assets/{mermaid-parser.core-DxEa8E3F-akMN_vOP.js → mermaid-parser.core-DxEa8E3F-C7Z349Pq.js} +2 -2
  67. package/dist/assets/{mermaid.core-V0OYwIz3-BMPzsgXF.js → mermaid.core-V0OYwIz3-BAg6sjQI.js} +3 -3
  68. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-Bed_YDhf.js → mindmap-definition-LN4V7U3C-CexN3O6L-B4euhJCV.js} +1 -1
  69. package/dist/assets/packet-7NZHBO7P-CQI3flND-CQk0f108.js +1 -0
  70. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CBrM86uu.js → pegDiagram-2B236MQR-BiM4G0cW-Brrbufeb.js} +1 -1
  71. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-CpVaGdmz.js +1 -0
  72. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-D-tRTGSr.js → pieDiagram-ENE6RG2P-B_fBBS-2-DognIWKZ.js} +1 -1
  73. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CH-H4qGn.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-BPBwmAdC.js} +1 -1
  74. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CKmLEdNO.js +1 -0
  75. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-oIAJF7wn.js +1 -0
  76. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B4whEhXF.js +1 -0
  77. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DPRdZSTX.js +1 -0
  78. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-CbVKtvJy.js +1 -0
  79. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-jEmJgaFj.js → railroadDiagram-RFXS5EU6--lNIlG64-5B9Ve7w1.js} +1 -1
  80. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-DurumWyC.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-iRCTL6WF.js} +1 -1
  81. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-Doed1fks.js → sankeyDiagram-HTMAVEWB-DlutaXDv-BWx6oil_.js} +1 -1
  82. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-CmkZuV9D.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-Dyjw8haJ.js} +1 -1
  83. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-BFXZmQ_i.js → stateDiagram-2N3HPSRC-jf_dXUEU-DNMm4FaT.js} +1 -1
  84. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-DW90BptC.js +1 -0
  85. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-e_82UQvE.js → swimlanes-5IMT3BWC-CYjtALQH-0aqXIsbo.js} +1 -1
  86. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-WTqIMZw2.js +8 -0
  87. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CO0rLezO.js → timeline-definition-FHXFAJF6-DYJ4oUm8-BEW3_bqU.js} +1 -1
  88. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-IkkpQPxV.js +1 -0
  89. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DgR55667.js +1 -0
  90. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-BO9jvKIz.js → vennDiagram-L72KCM5P-CdKAHoek-ftlvelkp.js} +1 -1
  91. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-C-7aQQUL.js +1 -0
  92. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-eXxyipXb.js → wardleyDiagram-EHGQE667-BF6c4_CW-ND9jAABP.js} +1 -1
  93. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-CMZLvO1e.js → xychartDiagram-FW5EYKEG-CGiKngj7-BbHq6IVI.js} +1 -1
  94. package/dist/index.html +2 -2
  95. package/package.json +15 -16
  96. package/server/agents/antigravity-session.ts +1 -1
  97. package/server/agents/antigravity-sessions.ts +4 -2
  98. package/server/agents/codex-activity.ts +1 -1
  99. package/server/agents/codex-rate-limits.spec.ts +1 -0
  100. package/server/agents/codex-rate-limits.ts +1 -1
  101. package/server/agents/codex-rollout.spec.ts +1 -0
  102. package/server/agents/codex-session.ts +4 -2
  103. package/server/agents/codex-sessions.ts +3 -2
  104. package/server/agents/probe-session.spec.ts +1 -0
  105. package/server/agents/probe-stall.spec.ts +1 -0
  106. package/server/agents/probe-transcript.spec.ts +11 -1
  107. package/server/agents/rate-limit-persist.spec.ts +1 -0
  108. package/server/agents/rate-limit-probe.spec.ts +1 -0
  109. package/server/agents/rate-limit-store.spec.ts +1 -0
  110. package/server/agents/statusline.spec.ts +1 -0
  111. package/server/backends/collections.ts +5 -3
  112. package/server/backends/remoteHost/attachment-path.ts +1 -1
  113. package/server/backends/remoteHost/handlers/getCollection.ts +2 -1
  114. package/server/backends/remoteHost/handlers/getFeed.ts +2 -1
  115. package/server/backends/remoteHost/handlers/getRemoteView.ts +3 -2
  116. package/server/backends/remoteHost/handlers/getRemoteViewItems.ts +3 -2
  117. package/server/backends/remoteHost/handlers/issueWork.ts +6 -4
  118. package/server/backends/remoteHost/handlers/mutateRemoteView.ts +3 -2
  119. package/server/backends/remoteHost/issueWork.spec.ts +2 -1
  120. package/server/backends/remoteHost/routes.spec.ts +1 -0
  121. package/server/backends/remoteHost/session.spec.ts +1 -0
  122. package/server/backends/remoteHost/skills.ts +1 -1
  123. package/server/backends/remoteHost/terminalInput.ts +1 -1
  124. package/server/backends/remoteView.ts +2 -1
  125. package/server/backends/scheduler.ts +1 -0
  126. package/server/backends/translation.ts +7 -3
  127. package/server/backends/wiki.ts +3 -2
  128. package/server/config/app-config.ts +9 -0
  129. package/server/config/config-body.ts +1 -0
  130. package/server/config/config-routes.ts +16 -1
  131. package/server/config/dir-config.ts +1 -1
  132. package/server/config/header-title.ts +2 -2
  133. package/server/config/hue-rotate.ts +78 -0
  134. package/server/config/workspace.ts +2 -1
  135. package/server/config/worktree-dir-config.ts +74 -0
  136. package/server/files/backup-store.ts +5 -3
  137. package/server/files/files-browse.ts +5 -3
  138. package/server/files/pathContainment.ts +1 -1
  139. package/server/files/renderedDoc.ts +1 -0
  140. package/server/files/scripts.ts +1 -0
  141. package/server/git/forge-host.ts +18 -3
  142. package/server/git/forge-support.ts +8 -7
  143. package/server/git/git-parse.ts +2 -2
  144. package/server/git/gitRemote.ts +1 -1
  145. package/server/git/glab.ts +64 -12
  146. package/server/git/issue-work.ts +5 -3
  147. package/server/git/issues.ts +2 -2
  148. package/server/git/prPhase.ts +6 -6
  149. package/server/git/prs.ts +5 -4
  150. package/server/git/remote-ref.ts +1 -1
  151. package/server/git/work-comment.ts +7 -7
  152. package/server/git/worktree-routes.ts +6 -7
  153. package/server/git/worktrees.ts +26 -1
  154. package/server/index.ts +3 -2
  155. package/server/infra/accounting-tool.ts +2 -2
  156. package/server/infra/pluginRuntime.ts +1 -1
  157. package/server/infra/plugins-registry.ts +79 -25
  158. package/server/infra/process-guards.ts +5 -2
  159. package/server/infra/pty-env.ts +1 -0
  160. package/server/infra/pubsub.ts +2 -2
  161. package/server/infra/server-tool-load.ts +7 -2
  162. package/server/infra/tmux.ts +2 -2
  163. package/server/routes/gui-mcp-routes.ts +4 -2
  164. package/server/routes/hook-routes.ts +34 -24
  165. package/server/routes/issue-work-routes.ts +2 -1
  166. package/server/routes/mcp-routes.ts +2 -2
  167. package/server/routes/requestBody.ts +14 -0
  168. package/server/routes/session-routes.ts +14 -2
  169. package/server/session/cleared-transcripts.ts +1 -1
  170. package/server/session/command-summary.ts +1 -1
  171. package/server/session/decision-scan.ts +4 -1
  172. package/server/session/decisions.ts +1 -1
  173. package/server/session/last-turn.ts +6 -3
  174. package/server/session/launcher-gui-mcp.ts +1 -1
  175. package/server/session/pty-connection.ts +17 -12
  176. package/server/session/registry.ts +1 -1
  177. package/server/session/scheduled-sessions.ts +1 -1
  178. package/server/session/screen-rows.ts +4 -2
  179. package/server/session/session-reads.ts +3 -2
  180. package/server/session/shell-command.ts +1 -1
  181. package/server/session/tool-hook.ts +6 -3
  182. package/server/session/tool-store.ts +28 -9
  183. package/server/session/transcript.ts +12 -7
  184. package/server/session/translation-worker.ts +8 -4
  185. package/server/skills/mulmoterminal-bug-report/faq.md +16 -0
  186. package/server/skills/mulmoterminal-config/SKILL.md +23 -1
  187. package/server/skills/mulmoterminal-dirs/SKILL.md +17 -0
  188. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-B89nYL-T.js +0 -1
  189. package/dist/assets/channel-7wUqSdoX-DLHsiYx_.js +0 -1
  190. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-vnBEnnpL.js +0 -1
  191. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-vnBEnnpL.js +0 -1
  192. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-U6pbCywv.js +0 -1
  193. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-Dz8P7VVP.js +0 -1
  194. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Bqi3MvHN.js +0 -1
  195. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-C_bvrqXL.js +0 -1
  196. package/dist/assets/index-CXYjAh9r.css +0 -1
  197. package/dist/assets/index-D9sJGCR9.js +0 -616
  198. package/dist/assets/info-DKCQHKI2-Dplx5kMp-aO_hAsb7.js +0 -1
  199. package/dist/assets/packet-7NZHBO7P-CQI3flND-jjlc375I.js +0 -1
  200. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BGKp68D7.js +0 -1
  201. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-ChTyqNRH.js +0 -1
  202. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BRepafjd.js +0 -1
  203. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B2E_hGfU.js +0 -1
  204. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2k_5LtZ.js +0 -1
  205. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DSwMdPCz.js +0 -1
  206. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CZroC0CQ.js +0 -1
  207. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-C2aatMMX.js +0 -8
  208. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DhW5sK_F.js +0 -1
  209. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-oZY-xCg-.js +0 -1
  210. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BCWqjXDb.js +0 -1
package/README.md CHANGED
@@ -39,7 +39,7 @@ npx mulmoterminal@latest # starts on http://localhost:34567 and opens you
39
39
 
40
40
  ![MulmoTerminal's grid view — four live Claude sessions running side by side, each in its own color-coded project](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-2x2-live.png)
41
41
 
42
- *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working / done (blue), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
42
+ *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working (blue), done (green), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
43
43
 
44
44
  ## Why you'll want it
45
45
 
@@ -214,7 +214,7 @@ Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
214
214
  | **Required** | [`claude`](https://claude.com/claude-code) | every Claude session — this app is a cockpit for it | `npm i -g @anthropic-ai/claude-code`, then run `claude` once to log in |
215
215
  | **Required** | `git` | [worktree isolation](#git-worktrees--pull-requests), each cell's branch / unsaved-dot / diff readout, the PR footer | `brew install git` · `sudo apt install git` · `sudo dnf install git` · Windows: [git-scm.com](https://git-scm.com/download/win) |
216
216
  | **Required** | `gh` | the cross-repo **PRs & Issues** view and one-click PR creation — it uses your `gh` login, so no token is stored | [cli.github.com](https://cli.github.com), then `gh auth login` |
217
- | Optional | `glab` | the same for **gitlab.com** projects (#981). Same arrangement: the CLI holds the credentials, this app stores no token | `brew install glab`, then `glab auth login` |
217
+ | Optional | `glab` | the same for **GitLab** projects (#981) — gitlab.com, and a self-hosted instance you declare in `gitlabHosts` (#1332). Same arrangement: the CLI holds the credentials, this app stores no token | `brew install glab`, then `glab auth login` (self-hosted: `glab auth login --hostname gitlab.example.com`) |
218
218
  | Recommended | `tmux` | [session persistence](#session-persistence-tmux) — terminals survive a server restart | `brew install tmux` · `sudo apt install tmux` · `sudo dnf install tmux` · no native Windows build (falls back to plain PTYs) |
219
219
  | Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
220
220
  | Optional | `ffmpeg` | video rendering from the [mulmo-script panel](#wiki-collections--the-gui-panel) (its plugin ships enabled) | `brew install ffmpeg` · `sudo apt install ffmpeg` · `sudo dnf install ffmpeg` |
@@ -543,6 +543,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
543
543
  | `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
544
544
  | `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
545
545
  | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates, using whichever CLI the host needs — your own `gh` or `glab` login, so no token is stored here. An entry may name its host — `gitlab.com/group/project` is read with `glab`, and work can be started on it, commented on and turned into a merge request. A host that is neither shows a row saying so. |
546
+ | `gitlabHosts` | Hosts that run a **self-hosted GitLab**, e.g. `["gitlab.example.com"]`. Nothing in a URL says which forge a host runs, so declaring it is what lets `prRepos` entries on that host be read with `glab` — everything gitlab.com can do, it can do. Needs `glab auth login --hostname <host>`. config.json only (no Settings control), so a hand edit takes effect on the next server start. |
546
547
  | `repoDirs` | `{ "owner/repo": "/abs/path" }` — which local clone work on a repo starts in, when you keep several side by side. Only the *choice* is stored; which clones exist is re-derived from `cwdPresets` on every read, and an entry that no longer names a clone of that repo is ignored. |
547
548
  | `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides the agents — any interactive command. A plain shell needs no entry: the launch form's **Shell** toggle opens `$SHELL` unconfigured. |
548
549
  | `quickCommands` | `{ label, text, agents? }` phrases the **phone** offers as chips on a session's terminal view. Tapping one puts `text` in the input box; it is not sent until you press send. `agents` (`"claude"` / `"codex"` / `"shell"`) scopes a chip to session kinds — omit it to offer the chip everywhere. Empty by default. |
@@ -740,22 +741,21 @@ yarn dev # backend (:34567) + Vite UI (:6856), concurrently — o
740
741
  yarn dev:server # backend only (node --import tsx --env-file-if-exists=.env server/index.ts)
741
742
  yarn dev:client # Vite dev server only
742
743
 
743
- yarn build # type-check (vue-tsc) + vite build -> dist/
744
- yarn typecheck:server # type-check the server (tsconfig.server.json)
745
- yarn typecheck:test # type-check the specs (tsconfig.test*.json)
744
+ yarn typecheck # type-check everything (vue-tsc -b)
745
+ yarn build # type-check + vite build -> dist/
746
746
  yarn server # run backend; serves dist/ + the APIs on :34567
747
747
  yarn test # vitest run
748
748
  ```
749
749
 
750
- The backend is TypeScript run directly via `tsx` (no build step); `server/` is
751
- type-checked separately through `tsconfig.server.json` (`strict`), kept out of
752
- the main `build` so the two type-check independently.
753
-
754
- Specs sit outside both of those projects, and vitest strips types rather than
755
- checking them — so `yarn typecheck:test` is what keeps them honest. It mirrors
756
- the same split: `tsconfig.test.json` (client specs, DOM + `.vue`) and
757
- `tsconfig.test-server.json` (server specs, node). CI runs it alongside the
758
- other two.
750
+ `yarn typecheck` covers the whole repo. The root `tsconfig.json` is a solution
751
+ file that references all five projects, so one `vue-tsc -b` builds them:
752
+ `tsconfig.app.json` (client), `tsconfig.node.json` (vite config),
753
+ `tsconfig.server.json` (backend, run directly via `tsx` with no build step),
754
+ plus `tsconfig.test.json` and `tsconfig.test-server.json` for the specs — which
755
+ need checking of their own because vitest strips types rather than checking
756
+ them. They exist as separate projects because each has its own compiler options
757
+ (the client ones DOM + `.vue`, the server ones node, the specs with
758
+ `noUncheckedIndexedAccess` off).
759
759
 
760
760
  In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
761
761
  `:34567`. In production, run `yarn build` then `yarn server` and open
@@ -920,6 +920,17 @@ separate working tree that shares the repo's `.git`, so several agents can work
920
920
  repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
921
921
  `MULMOTERMINAL_HOME`), and existing ones are listed below the field.
922
922
 
923
+ **A worktree inherits the project's settings.** `.mulmoterminal.json` is normally gitignored,
924
+ so a fresh worktree used to have none — no colours, no name, no model, no grid rank. It is now
925
+ given its own copy derived from the project's: `name` / `theme` / `colors` / `fontSize` /
926
+ `fontFamily` / `provider` / `model` as written, the seven chrome colours **rotated 12 degrees
927
+ further around the hue wheel per worktree** (so a project's trees read as a gradient; a grey like
928
+ `#ffffff` has no hue to move and stays put), and `orderPriority` at the project's rank **+ 1**, so
929
+ the worktree sorts directly after it. `sound` / `sounds` / `addDirs` are not carried — they name
930
+ paths inside the project directory. Written only where git would **ignore** it: an untracked file
931
+ in a worktree's `git status` would make it count as dirty, and a dirty worktree is one
932
+ MulmoTerminal refuses to remove. An existing config in the worktree is never overwritten.
933
+
923
934
  **One worktree, one session.** A worktree is tied to a branch, so it is never started
924
935
  twice: a listed row **resumes** that worktree's session when it has one, and **starts** one
925
936
  only when it has none. A row whose session is open in another terminal reads `in use` and
@@ -968,7 +979,7 @@ Closing a worktree cell asks whether to **keep** the worktree or **discard & rem
968
979
 
969
980
  **PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
970
981
  view that aggregates open PRs **and** issues across the repos listed in Settings →
971
- **Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project`) via your server-side `gh` / `glab` login.
982
+ **Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project` — plus any host declared in `gitlabHosts`) via your server-side `gh` / `glab` login.
972
983
  PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
973
984
  issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
974
985
  independently. Backed by `GET /api/prs` and `GET /api/issues`.
@@ -1309,7 +1320,7 @@ same-origin-guarded.
1309
1320
  | `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
1310
1321
  | `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
1311
1322
  | `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>` — or, with `issue: <N>`, on `issue/<N>-<slug>` forked from a freshly fetched `origin/<base>`; remove (managed root only), push, open a PR (`gh`, else compare URL). |
1312
- | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for `gitlab.com/...` ones. |
1323
+ | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for gitlab.com and any host declared in `gitlabHosts`. |
1313
1324
  | `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
1314
1325
  | `POST /api/issues/start` | Cut an issue's worktree in one of that repo's known clones and spawn a session there, seeded with the issue as a draft. |
1315
1326
  | `GET /api/github/star` · `POST /api/github/star` | Whether you have starred MulmoTerminal, and star it (via `gh`). `starred: null` means `gh` could not answer, and hides the button. |
@@ -0,0 +1,11 @@
1
+ // Sort filenames and directory names deterministically.
2
+ //
3
+ // `.sort()` with no comparator is flagged (sonarjs/no-alphabetical-sort) because the default is
4
+ // implementation-defined for non-ASCII. The rule suggests `localeCompare`, which would be WRONG
5
+ // here: these are filenames — zero-padded date directories and ISO-stamped rollout files — and
6
+ // ordering them by the user's locale makes the answer depend on who is running the app. Code-unit
7
+ // order is what the callers actually mean, and it is the same everywhere.
8
+ export function byCodeUnit(a: string, b: string): number {
9
+ if (a < b) return -1;
10
+ return a > b ? 1 : 0;
11
+ }
@@ -59,8 +59,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
59
59
  const lines = markdown.split("\n");
60
60
  const blocks: FencedBlock[] = [];
61
61
  let i = 0;
62
+ // Each line is read into a const before use: with noUncheckedIndexedAccess, `lines[i]` is
63
+ // `string | undefined` even inside `i < lines.length`, and the loop condition is what actually
64
+ // rules the undefined out.
62
65
  while (i < lines.length) {
63
- const open = fenceOf(lines[i]);
66
+ const line = lines[i];
67
+ const open = line === undefined ? null : fenceOf(line);
64
68
  if (!open) {
65
69
  i++;
66
70
  continue;
@@ -69,12 +73,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
69
73
  const info = open.info.trim();
70
74
  const body: string[] = [];
71
75
  i++;
72
- while (i < lines.length && !closes(lines[i], fence)) {
73
- body.push(lines[i]);
76
+ for (let next = lines[i]; next !== undefined && !closes(next, fence); next = lines[i]) {
77
+ body.push(next);
74
78
  i++;
75
79
  }
76
80
  i++; // step over the closing fence (or past the end, for an unclosed block)
77
- blocks.push({ lang: info ? (info.split(/\s+/)[0].toLowerCase() ?? null) : null, body: body.join("\n") });
81
+ blocks.push({ lang: info ? (info.split(/\s+/)[0]?.toLowerCase() ?? null) : null, body: body.join("\n") });
78
82
  }
79
83
  return blocks;
80
84
  }
@@ -84,5 +88,5 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
84
88
  * exactly like a copy that silently failed. */
85
89
  export function lastFencedBlock(markdown: string): FencedBlock | null {
86
90
  const withBody = fencedBlocks(markdown).filter((b) => b.body.trim() !== "");
87
- return withBody.length ? withBody[withBody.length - 1] : null;
91
+ return withBody[withBody.length - 1] ?? null;
88
92
  }
@@ -0,0 +1,60 @@
1
+ // Which hosts are a GitLab this app can talk to (#1332). `gitlab.com` is known from its name; a
2
+ // self-hosted instance at `gitlab.hogefuga.com` is indistinguishable from any other host, so the
3
+ // user DECLARES it in `gitlabHosts` — the config mechanism #981 said this would need.
4
+ //
5
+ // In `common/` because both sides decide from it: the server picks a CLI from it, and the browser
6
+ // says why a control is off. Two copies would drift, and disagreeing is the failure the forge
7
+ // abstraction exists to undo.
8
+ import { GITHUB_HOST, GITLAB_HOST } from "./repoEntry.js";
9
+
10
+ // A hostname, lower case, with at least one dot. The dot is not cosmetic: `parseRepoEntry` reads a
11
+ // leading segment as a host ONLY when it contains one, so a dotless declaration could never match
12
+ // an entry and would sit in the config doing nothing.
13
+ const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
14
+
15
+ const SCHEME_RE = /^https?:\/\//;
16
+
17
+ const GITLAB_HOSTS_MAX = 20;
18
+
19
+ /** One declared host, or null when the string does not name one.
20
+ *
21
+ * A pasted `https://gitlab.example.com/` is accepted and reduced to its hostname: this value is
22
+ * hand-typed into config.json with no UI to validate it, and the browser's address bar is where
23
+ * the user is copying from. Anything else with a slash is rejected rather than truncated — a
24
+ * project path here is a different mistake, and silently keeping its host would hide it.
25
+ */
26
+ export function normalizeGitlabHost(input: unknown): string | null {
27
+ if (typeof input !== "string") return null;
28
+ const host = input.trim().toLowerCase().replace(SCHEME_RE, "").replace(/\/$/, "");
29
+ return HOSTNAME_RE.test(host) ? host : null;
30
+ }
31
+
32
+ /** The declared hosts, de-duplicated and capped. Anything unusable is dropped rather than failing
33
+ * the whole config — one typo must not cost the user the hosts they spelled correctly. */
34
+ export function sanitizeGitlabHosts(input: unknown): string[] {
35
+ if (!Array.isArray(input)) return [];
36
+ const hosts = new Set<string>();
37
+ for (const value of input) {
38
+ const host = normalizeGitlabHost(value);
39
+ if (host) hosts.add(host);
40
+ if (hosts.size >= GITLAB_HOSTS_MAX) break;
41
+ }
42
+ return [...hosts];
43
+ }
44
+
45
+ /** Whether `host` is a GitLab: gitlab.com, or one the user declared.
46
+ *
47
+ * `github.com` is refused whatever the config says. A declaration is a hand-typed line with no UI
48
+ * behind it, and the one that would do real damage — every GitHub repo suddenly addressed with
49
+ * `glab` — is also the easiest to write by accident.
50
+ */
51
+ export const isGitlabHost = (host: string, declared: readonly string[]): boolean => host === GITLAB_HOST || (host !== GITHUB_HOST && declared.includes(host));
52
+
53
+ /** Why a host is not handled, and what to do about it.
54
+ *
55
+ * ONE sentence, shared by the PRs/Issues row and the start-work refusal: #1332 was filed by a user
56
+ * who read "is not supported yet" and could not tell whether their setup was wrong, whether the
57
+ * feature was coming, or what to try — so the reason has to carry the fix.
58
+ */
59
+ export const unknownForgeReason = (host: string): string =>
60
+ `${host} is not supported yet — MulmoTerminal reads github.com and gitlab.com; if ${host} is a self-hosted GitLab, add it to "gitlabHosts" in ~/.mulmoterminal/config.json and restart`;
@@ -11,3 +11,9 @@ import { isObject } from "graphai";
11
11
  // graphai's own `isPlainObject` is exactly this (it also rejects Map/Date/class instances), but
12
12
  // it is not exported from the package root, so the array check stays here.
13
13
  export const isRecord = (value: unknown): value is Record<string, unknown> => isObject(value) && !Array.isArray(value);
14
+
15
+ // "Absent, or of this type" — what an OPTIONAL field on a wire shape has to satisfy. A guard that
16
+ // skips these asserts a type it has not seen: absent is meaningful (an older server never said the
17
+ // field), but a present one of the wrong type is a lie the rest of the code then reads.
18
+ export const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
19
+ export const optionalBoolean = (value: unknown): boolean => value === undefined || typeof value === "boolean";
@@ -0,0 +1,8 @@
1
+ // The array counterpart to isRecord, and it exists for a reason that is easy to miss:
2
+ // `Array.isArray(value)` on an `unknown` narrows it to **`any[]`**, not `unknown[]`. So the
3
+ // obvious `Array.isArray(x) ? x : []` hands back an array whose elements are outside the type
4
+ // checker entirely, and every read off them type-checks against anything.
5
+ //
6
+ // Declaring the guard's result as `unknown[]` keeps the elements unverified-but-visible, which is
7
+ // what the callers want: they check each element before using it.
8
+ export const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value);
@@ -8,15 +8,17 @@
8
8
  // docs/remote-host-protocol.md), so it refuses `choose` where the desktop opens it.
9
9
  import type { RepoDirs } from "./repoDirs";
10
10
  import { GITHUB_HOST, GITLAB_HOST, parseRepoEntry } from "./repoEntry";
11
+ import { isGitlabHost, unknownForgeReason } from "./gitlabHosts";
11
12
 
12
13
  // Where work can be STARTED — the issue is read and a worktree cut. A host that is merely listable
13
- // still refuses, and says which host it is rather than blaming a missing clone (#981).
14
- const STARTABLE_HOSTS: ReadonlySet<string> = new Set([GITHUB_HOST, GITLAB_HOST]);
14
+ // still refuses, and says which host it is rather than blaming a missing clone (#981). A host the
15
+ // user declared as self-hosted GitLab starts work like gitlab.com does (#1332).
16
+ const isStartableHost = (host: string, gitlabHosts: readonly string[]): boolean => host === GITHUB_HOST || isGitlabHost(host, gitlabHosts);
15
17
 
16
- /** The hosts named in a refusal, read off the set rather than written out beside it — a hardcoded
17
- * "github.com only" survived GitLab becoming startable and told users the opposite of the truth
18
- * (Codex review). Exported so the phone's refusal says the same thing. */
19
- export const startableHosts = (): string => [...STARTABLE_HOSTS].join(" and ");
18
+ /** The hosts named in a refusal, built from the same rule rather than written out beside it — a
19
+ * hardcoded "github.com only" survived GitLab becoming startable and told users the opposite of
20
+ * the truth (Codex review). Exported so the phone's refusal says the same thing. */
21
+ export const startableHosts = (gitlabHosts: readonly string[]): string => [...new Set([GITHUB_HOST, GITLAB_HOST, ...gitlabHosts])].join(" and ");
20
22
 
21
23
  export type IssueStartPlan =
22
24
  /** No clone of this repo on this machine, so there is nowhere for the work to happen. */
@@ -35,24 +37,29 @@ export type BlockedIssueStartPlan = Exclude<IssueStartPlan, { kind: "ready" }>;
35
37
 
36
38
  // `repo` is REQUIRED, not optional, even though only one branch reads it: a caller that omitted it
37
39
  // would get `no-clone` for a GitLab repo, which is the exact wrong answer this parameter was added
38
- // to fix. Making the compiler ask for it is what stops that coming back.
39
- export function issueStartPlan(entry: RepoDirs | undefined, repo: string): IssueStartPlan {
40
+ // to fix. Making the compiler ask for it is what stops that coming back. `gitlabHosts` is required
41
+ // for the same reason — defaulted to empty, a caller that forgot it would refuse the user's own
42
+ // self-hosted GitLab and blame the host (#1332).
43
+ export function issueStartPlan(entry: RepoDirs | undefined, repo: string, gitlabHosts: readonly string[]): IssueStartPlan {
40
44
  // Checked before the clone list, because a GitLab repo has no entry there and would otherwise
41
45
  // read as "no clone" — a reason that is not true and points at the wrong fix.
42
46
  const parsed = parseRepoEntry(repo);
43
- if (parsed && !STARTABLE_HOSTS.has(parsed.host)) return { kind: "unsupported-forge", host: parsed.host };
47
+ if (parsed && !isStartableHost(parsed.host, gitlabHosts)) return { kind: "unsupported-forge", host: parsed.host };
44
48
  const dirs = entry?.dirs ?? [];
45
49
  if (dirs.length === 0) return { kind: "no-clone" };
46
50
  // A recording wins even when the repo has one clone: it is still the same answer, and treating
47
51
  // the two cases the same keeps "recorded" from being a state the UI has to distinguish.
48
52
  const recorded = entry?.primary;
49
53
  if (recorded) return { kind: "ready", dir: recorded };
50
- if (dirs.length === 1) return { kind: "ready", dir: dirs[0].path };
54
+ const only = dirs.length === 1 ? dirs[0] : undefined;
55
+ if (only) return { kind: "ready", dir: only.path };
51
56
  return { kind: "choose", dirs };
52
57
  }
53
58
 
54
59
  /** Why the control is disabled, for the row's title text. Null when it is not disabled. */
55
60
  export function issueStartBlockedReason(plan: IssueStartPlan, repo: string): string | null {
56
- if (plan.kind === "unsupported-forge") return `Starting work supports ${startableHosts()} — ${plan.host} issues are listed here, not started from`;
61
+ // The same sentence the row's own error carries: the host is not one this app knows, and the fix
62
+ // is a config line rather than anything about this issue or this clone (#1332).
63
+ if (plan.kind === "unsupported-forge") return unknownForgeReason(plan.host);
57
64
  return plan.kind === "no-clone" ? `No local clone of ${repo} — add one to your directory presets to start work here` : null;
58
65
  }
package/common/keymap.ts CHANGED
@@ -87,6 +87,7 @@ export function parseKeyBinding(input: string): KeyBinding | null {
87
87
  const parts = input.split("+").map((part) => part.trim());
88
88
  if (parts.length === 0 || parts.some((part) => part === "")) return null;
89
89
  const key = parts[parts.length - 1];
90
+ if (key === undefined) return null; // unreachable: parts.length was checked above
90
91
  const binding: KeyBinding = { key, shift: false, alt: false, ctrl: false, meta: false };
91
92
  for (const part of parts.slice(0, -1)) {
92
93
  const flag = MODIFIERS[part.toLowerCase()];
@@ -226,6 +227,7 @@ function duplicateWarnings(bound: Map<string, Claim[]>): KeymapProblem[] {
226
227
  return [...bound.values()].flatMap((claims) => {
227
228
  if (claims.length < 2) return [];
228
229
  const [winner, ...losers] = [...claims].sort((a, b) => a.rank - b.rank);
230
+ if (!winner) return []; // unreachable: claims.length >= 2 was checked above
229
231
  return losers.map(({ label, binding }) => ({
230
232
  action: label,
231
233
  binding,
package/common/median.ts CHANGED
@@ -8,6 +8,11 @@ export const median = (values: number[]): number | null => {
8
8
  if (values.length === 0) return null;
9
9
  const sorted = [...values].sort((a, b) => a - b);
10
10
  const mid = Math.floor(sorted.length / 2);
11
- if (sorted.length % 2 === 1) return sorted[mid];
12
- return (sorted[mid - 1] + sorted[mid]) / 2;
11
+ // Read once each: `sorted[mid]` is `number | undefined` under noUncheckedIndexedAccess, and the
12
+ // length checks above are what actually rule that out.
13
+ const middle = sorted[mid];
14
+ if (middle === undefined) return null;
15
+ if (sorted.length % 2 === 1) return middle;
16
+ const below = sorted[mid - 1];
17
+ return below === undefined ? middle : (below + middle) / 2;
13
18
  };
package/common/prPhase.ts CHANGED
@@ -83,7 +83,7 @@ function toIssueNumber(digits: string): number | null {
83
83
  // shown next to this cell's PR has to belong to the same repo to be clickable and true.
84
84
  export function issueRefFromPrBody(body: string | null | undefined): number | null {
85
85
  const found = typeof body === "string" ? CLOSING_KEYWORD.exec(body) : null;
86
- return found ? toIssueNumber(found[1]) : null;
86
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
87
87
  }
88
88
 
89
89
  // Whether the body ALREADY states what merging it closes, in either form GitHub honours. Broader
@@ -105,7 +105,7 @@ const BRANCH_ISSUE = /^[a-z][a-z-]*\/([1-9]\d*)-/;
105
105
  // somebody else's issue. Named for the doubt so a call site can't forget it.
106
106
  export function issueCandidateFromBranch(branch: string | null | undefined): number | null {
107
107
  const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
108
- return found ? toIssueNumber(found[1]) : null;
108
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
109
109
  }
110
110
 
111
111
  // The prefix this app gives a branch it creates FOR an issue (#1171). Shared because the two
@@ -121,5 +121,5 @@ const ANCHORED_ISSUE = new RegExp(`^${ISSUE_BRANCH_PREFIX}([1-9]\\d*)-`);
121
121
  // else's issue the moment the PR merges, which no amount of confirming afterwards undoes.
122
122
  export function issueFromAnchoredBranch(branch: string | null | undefined): number | null {
123
123
  const found = typeof branch === "string" ? ANCHORED_ISSUE.exec(branch) : null;
124
- return found ? toIssueNumber(found[1]) : null;
124
+ return found?.[1] === undefined ? null : toIssueNumber(found[1]);
125
125
  }
@@ -0,0 +1,28 @@
1
+ // Reading a string out of untrusted JSON — a phone's command params, a transcript line, a `gh`
2
+ // response — without `String()`.
3
+ //
4
+ // `String(value)` answers for ANY input, and that is the problem: an object becomes the literal
5
+ // text "[object Object]" and an array becomes "1,2". Nothing throws, so the bad value travels —
6
+ // as a collection slug that matches nothing, or as a session title on screen. The rule that
7
+ // catches this is @typescript-eslint/no-base-to-string.
8
+ //
9
+ // Reading returns "" (or null) instead, which the callers already handle: they were written
10
+ // against `String(x ?? "")`, so an absent field was always the empty string.
11
+
12
+ /** The value when it is a string, else `""`. For a field the caller then validates or defaults. */
13
+ export const readString = (value: unknown): string => (typeof value === "string" ? value : "");
14
+
15
+ /**
16
+ * A value rendered for a HUMAN — an error message naming what was rejected.
17
+ *
18
+ * Here the input is bad by definition, so there is something to say about any of it; `String()` is
19
+ * wrong only because "[object Object]" tells the reader nothing. JSON keeps the shape visible.
20
+ */
21
+ export const describeValue = (value: unknown): string => {
22
+ if (typeof value === "string") return value;
23
+ try {
24
+ return JSON.stringify(value) ?? String(value);
25
+ } catch {
26
+ return "[unserializable]"; // a circular structure, which JSON.stringify throws on
27
+ }
28
+ };
@@ -30,8 +30,9 @@ export interface RepoEntry {
30
30
  export function parseRepoEntry(entry: string): RepoEntry | null {
31
31
  const segments = entry.trim().split("/");
32
32
  if (segments.some((segment) => segment === "")) return null;
33
- const declared = segments.length > 1 && HOST_SEGMENT.test(segments[0]);
34
- return declared ? { host: segments[0].toLowerCase(), path: segments.slice(1), declared } : { host: GITHUB_HOST, path: segments, declared };
33
+ const first = segments[0];
34
+ const host = segments.length > 1 && first !== undefined && HOST_SEGMENT.test(first) ? first : null;
35
+ return host === null ? { host: GITHUB_HOST, path: segments, declared: false } : { host: host.toLowerCase(), path: segments.slice(1), declared: true };
35
36
  }
36
37
 
37
38
  /** The identity a repository is known by ON ITS OWN HOST — `owner/repo` for GitHub, the group path
@@ -1 +1 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-akMN_vOP.js";import"./chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js";import"./chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().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=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
1
+ import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DxEa8E3F-C7Z349Pq.js";import"./chunk-WYO6CB5R-BlzOfotS-CbdIi1Rx.js";import"./chunk-VAUOI2AC-CS9QJ4yz-o20b35Qf.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-BwgEBlFh.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().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=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import{f as e}from"./mermaid-parser.core-DxEa8E3F-C7Z349Pq.js";export{e as createArchitectureServices};