mulmoterminal 4.23.0 → 4.25.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 (182) hide show
  1. package/README.md +16 -8
  2. package/bin/agent-bins.d.ts +15 -0
  3. package/bin/agent-bins.js +38 -0
  4. package/bin/cli-args.d.ts +1 -1
  5. package/bin/cli-args.js +7 -2
  6. package/bin/default-agent.d.ts +19 -0
  7. package/bin/default-agent.js +99 -0
  8. package/bin/has-command.d.ts +9 -0
  9. package/bin/has-command.js +120 -0
  10. package/bin/mulmoterminal.js +71 -32
  11. package/common/defaultAgent.ts +31 -0
  12. package/common/toolGroups.ts +9 -8
  13. package/dist/assets/{AllPackages-CF2GkZ5g-Dcf5cDN0.js → AllPackages-B1YCJRYB-D8E2jiS6.js} +1 -1
  14. package/dist/assets/{BaseConfiguration-CAwZOAki-DUdHJKsk.js → BaseConfiguration-B6s_tKOW-2eCrGC8V.js} +1 -1
  15. package/dist/assets/{Element-BMI7Nj5w-hV6e9MWX.js → Element-BMI7Nj5w-Cb2Ekmhb.js} +1 -1
  16. package/dist/assets/{Entities-C8p_0jCn-B8iGcgdn.js → Entities-C8p_0jCn-pRbhKHQD.js} +1 -1
  17. package/dist/assets/{FunctionList-YWk54TCy-BirKP0I_.js → FunctionList-YWk54TCy-BzSIvZ-T.js} +1 -1
  18. package/dist/assets/{InputJax-C5dj-ntL-Darzu8r-.js → InputJax-C5dj-ntL-JrwIzEIc.js} +1 -1
  19. package/dist/assets/{MathItem-YJvQL-XJ-BelhLvBn.js → MathItem-YJvQL-XJ-BschBN6g.js} +1 -1
  20. package/dist/assets/{MmlFactory-DuXYIgx8-djfJFX6f.js → MmlFactory-DSoKkE7s-Cvf26hUZ.js} +1 -1
  21. package/dist/assets/{Options-oARQeeuT-J_ILSCvw.js → Options-oARQeeuT-oG2rsyL_.js} +1 -1
  22. package/dist/assets/{OutputJax-DfUZNNCu-DtuXVd03.js → OutputJax-DfUZNNCu-BHj0ddTi.js} +1 -1
  23. package/dist/assets/{PrioritizedList-6lun1Xqd-BilCpUSB.js → PrioritizedList-6lun1Xqd-8AcZMMAd.js} +1 -1
  24. package/dist/assets/{Styles-BPXeoqzi-CdLkCiVG.js → Styles-5NmwS-3M-BcRajaa1.js} +1 -1
  25. package/dist/assets/{TeXAtom-LHT3KFWH-BXMHOoT3.js → TeXAtom-B5ni9myz-Dbw_2bQk.js} +1 -1
  26. package/dist/assets/{abnfDiagram-VCTEODGH-DoYNjdBB-B7ZIBaJ6.js → abnfDiagram-VCTEODGH-BGhi-oVb-Dki-UFbe.js} +1 -1
  27. package/dist/assets/{arc-CqByPKkF-FwfFFIr8.js → arc-CK5ru50N-BytqUmIH.js} +1 -1
  28. package/dist/assets/architecture-7GRP2DOG-otSkCkT5-BHw2BeHi.js +1 -0
  29. package/dist/assets/architectureDiagram-5GKGNRK7-ELuPYMlf-Bkbt65bl.js +36 -0
  30. package/dist/assets/{assistive-mml-DXJXho4o-rl36c5mB.js → assistive-mml-CdU3rUpx-DXMsoK-e.js} +1 -1
  31. package/dist/assets/{blockDiagram-I7D4REHJ-1Cng0Q4K-_Juv9OUQ.js → blockDiagram-I7D4REHJ-BZ7-OGWQ-BUcazgSJ.js} +4 -4
  32. package/dist/assets/{c4Diagram-7LVT6UL2-CAb9c8nc-Bd9Kox9w.js → c4Diagram-7LVT6UL2-DCovPZ_4-CdSZg8V0.js} +1 -1
  33. package/dist/assets/channel-Bmu_OOc6-D6U8MyiH.js +1 -0
  34. package/dist/assets/chunk-4HAMMTFA-DqpzLmar-VEBaVttG.js +74 -0
  35. package/dist/assets/{chunk-75Z2AOVW-CbPA0Zt_-DVkb-raF.js → chunk-75Z2AOVW-CT5TYzp5-DV0JLVcu.js} +1 -1
  36. package/dist/assets/{chunk-DU6HZSFF-DuOx2LFX-DQp1Pge9.js → chunk-DU6HZSFF-BVf-rtdf-DZFdIv0b.js} +2 -2
  37. package/dist/assets/{chunk-F27PBJKO-CgBMw9yN-D7wnwed7.js → chunk-F27PBJKO-rP8pUsk3-BX--T-KN.js} +1 -1
  38. package/dist/assets/{chunk-IMKFNOWR-ij1DSzMb-dYKuj_-S.js → chunk-IMKFNOWR-D_IT0r3G-DjS5gJyn.js} +2 -2
  39. package/dist/assets/{chunk-POPQ4Y6H-Bh_uOTmp-YP4Y0H7o.js → chunk-POPQ4Y6H-C5YURnHe-DkV_Qv-z.js} +1 -1
  40. package/dist/assets/{chunk-SHT3W25Y-B8dQLS5w-BYPNRBhs.js → chunk-SHT3W25Y-CfF0J8Qh-CqtkI0h1.js} +5 -5
  41. package/dist/assets/{chunk-SVP7TREG-B6A-T8Th-BYSVtNuZ.js → chunk-SVP7TREG-CTZbRIbX-CB9zieez.js} +1 -1
  42. package/dist/assets/{chunk-TICWLB2K-C1kwD29l-Br0bNia0.js → chunk-TICWLB2K-DP1lYmZm-No0ln892.js} +4 -4
  43. package/dist/assets/{chunk-XXDRQBXY-vg0kfC73-CPErv3MQ.js → chunk-XXDRQBXY-vg0kfC73--hFAc4CO.js} +1 -1
  44. package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-CJ40hHeh.js +1 -0
  45. package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-CJ40hHeh.js +1 -0
  46. package/dist/assets/{cose-bilkent-JH36ORCC-NuHCtE3P-h2_ltbMI.js → cose-bilkent-JH36ORCC-qIiD3RGD-KWysQw-T.js} +1 -1
  47. package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-D9cR0g5E.js +1 -0
  48. package/dist/assets/{cynefinDiagram-5FMLGOSQ-CA7sro34-DEmjN8U0.js → cynefinDiagram-5FMLGOSQ-BZrUCS-v-DBYfaO6r.js} +1 -1
  49. package/dist/assets/{cytoscape.esm-DuKOKbur-Cp2HF8G-.js → cytoscape.esm-Cu7uu3aJ-NgwPT4ez.js} +2 -2
  50. package/dist/assets/{dagre-B42rJlko-6iqQ6GWy.js → dagre-C2SZ8h4o-C9DP78vN.js} +1 -1
  51. package/dist/assets/dagre-GXQ25YYZ-CQcLM4na-Qand5Jrz.js +4 -0
  52. package/dist/assets/{diagram-S7CK7UJ4-D_nsLfRT-CG7iqcK9.js → diagram-S7CK7UJ4-BdmcmUQh-9BXoKyF6.js} +3 -3
  53. package/dist/assets/{diagram-UQ7AKVKN-DNN-YX9a-DPhn9r3f.js → diagram-UQ7AKVKN-BtvmEiYl-CumMQ6Hh.js} +2 -2
  54. package/dist/assets/{diagram-VSXAHHWV--HENzo-l-DiD97poT.js → diagram-VSXAHHWV-qEG6N0bV-OspSlthq.js} +2 -2
  55. package/dist/assets/{diagram-VX7I27RA-m2m9dROj-BKDeFJgq.js → diagram-VX7I27RA-BA8is49N-GNnFbfy3.js} +1 -1
  56. package/dist/assets/{diagram-Z3DM3KII-D4qpv4-B-D0DcEN8W.js → diagram-Z3DM3KII-XPcU7IOQ-C1Y1DeOE.js} +1 -1
  57. package/dist/assets/{dist-D1NuNlw7.js → dist-BFnpugto.js} +1 -1
  58. package/dist/assets/{dist-CyJfj5nA.js → dist-BWpvwH-7.js} +1 -1
  59. package/dist/assets/{dist-csgyobRf.js → dist-D9xZ1r8x.js} +1 -1
  60. package/dist/assets/{dist-DeiyD9Ki-CsdnEslR.js → dist-DeiyD9Ki-Da08vZiU.js} +1 -1
  61. package/dist/assets/{dist-iwCSUG0R.js → dist-WMBnnmLU.js} +1 -1
  62. package/dist/assets/{ebnfDiagram-PWID7BFC-CAbv3sZg-zYV7b2Vy.js → ebnfDiagram-PWID7BFC-CTHIHdBS-Beitg6Sa.js} +1 -1
  63. package/dist/assets/{erDiagram-RLTQ6QDP-a10Ryh0--Dq--2XRb.js → erDiagram-RLTQ6QDP-DVpWTNZj-CWPSP83Q.js} +2 -2
  64. package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-UhYn-qO9.js +1 -0
  65. package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-DFUJxE9K.js +1 -0
  66. package/dist/assets/{ganttDiagram-EL5Y4UJY-Cb7leeOC-BZul1n-G.js → ganttDiagram-EL5Y4UJY-BfAgk15F-DDG7lxNr.js} +1 -1
  67. package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-Gt9CfyXY.js +1 -0
  68. package/dist/assets/{gitGraphDiagram-WWUBYQGX-BCSbUpXG-Cd562rpn.js → gitGraphDiagram-WWUBYQGX-DEVVZ6wt-CkuBQcFO.js} +2 -2
  69. package/dist/assets/graphlib-BYLhHyYO-BDVf31O9.js +10 -0
  70. package/dist/assets/{html-DdQiKDqM-BsuwriGq.js → html-Z7dGbczU-cVi1MxGm.js} +1 -1
  71. package/dist/assets/{index-CDUg8mhn.js → index-CcvdOYTa.js} +381 -316
  72. package/dist/assets/info-A6RAGUB7-fR2SUJmJ-BW_9XUKB.js +1 -0
  73. package/dist/assets/{infoDiagram-27XIBGKW-BJRaZItN-Cm4T5Wwv.js → infoDiagram-27XIBGKW-CvHUhAHd-2jeed3SU.js} +1 -1
  74. package/dist/assets/{ishikawaDiagram-5VMMS53U-DHlFe__S-DGs6xnNx.js → ishikawaDiagram-5VMMS53U-CeqyKdwk-CX0dJEYY.js} +1 -1
  75. package/dist/assets/{journeyDiagram-3NMN7TZE-BOIo3JM--Bzc0IZn6.js → journeyDiagram-3NMN7TZE-Jfi7MPDi-GFbmO8nm.js} +1 -1
  76. package/dist/assets/{kanban-definition-UXKFOSKX-yrQdG683-Cct0Djrh.js → kanban-definition-UXKFOSKX-yOrlojns-BCGoM3TG.js} +4 -4
  77. package/dist/assets/{lengths-CvmPfBid-CzTSmCcr.js → lengths-CvmPfBid-UAX96cr7.js} +1 -1
  78. package/dist/assets/{linear-xsuGWu5q-CfNm2yv7.js → linear-jdmEy-qs-DH6G09Ig.js} +1 -1
  79. package/dist/assets/{liteAdaptor-C0FQiOEx-CTVB31Rv.js → liteAdaptor-pH5MXTG0-4ZCxV5Gl.js} +1 -1
  80. package/dist/assets/{mathjax-DVEo4UYj-Bp47CElr.js → mathjax-DVEo4UYj-pvH0Vaps.js} +1 -1
  81. package/dist/assets/{mermaid-parser.core-fkjeiYO7-P9xXkjhl.js → mermaid-parser.core-CzBW8Fof-BkJJkz2V.js} +5 -5
  82. package/dist/assets/mermaid.core-Dv4uylek-Cb3UvAUX.js +44 -0
  83. package/dist/assets/{mindmap-definition-YA3MSWOX-BpKAVyqs-d4jTnjxk.js → mindmap-definition-YA3MSWOX-DP_2R9Ga-CgqlDoWp.js} +2 -2
  84. package/dist/assets/{mo-CrlNnGXX-DC4IkWbc.js → mo-Xv_6CC7A-GIRpAipl.js} +1 -1
  85. package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-BFLakyCh.js +1 -0
  86. package/dist/assets/{pegDiagram-XKGWAZYB-C7eG_Ohl-Bq1D--Oy.js → pegDiagram-XKGWAZYB-BsBWryhi-Cao0_mmw.js} +1 -1
  87. package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-Die2SLux.js +1 -0
  88. package/dist/assets/{pieDiagram-E7YTZNPT-5f9wSfDR-DSt7pK7-.js → pieDiagram-E7YTZNPT-OugkS2pU-zbbK17Bn.js} +2 -2
  89. package/dist/assets/{quadrantDiagram-AXDQQJYC-mWeaOj5S-C-jM9gYY.js → quadrantDiagram-AXDQQJYC-DXEAAaUE-CtTU3K_Q.js} +1 -1
  90. package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-67POH3H6.js +1 -0
  91. package/dist/assets/railroad-74A4TZTK-6lEXVJo8-CFP2Gi-j.js +1 -0
  92. package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-y9Rrt74M.js +1 -0
  93. package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-66aczMob.js +1 -0
  94. package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-BwmZB9OP.js +1 -0
  95. package/dist/assets/{railroadDiagram-O6MQD6OU-DJskhE7u-myONQWUy.js → railroadDiagram-O6MQD6OU-BJys8O96-BIdHfmqT.js} +1 -1
  96. package/dist/assets/{requirementDiagram-BXWQKSXE-B5E0bU4h-D9fqLodu.js → requirementDiagram-BXWQKSXE-BaDgCtM5-M_Bk_Guz.js} +2 -2
  97. package/dist/assets/{sankeyDiagram-P5KCCOFB-DS3AOTai-BthgK6rY.js → sankeyDiagram-P5KCCOFB-puZsr_vK-Qb3kiw8c.js} +1 -1
  98. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bn2L9osv-DS7EWkDd.js → sequenceDiagram-WJ2MYXX4-D1ziEryC-BHonheSF.js} +6 -6
  99. package/dist/assets/{src-CLC0ddSd-BdwptT7W.js → src-CLC0ddSd-lXqARcUf.js} +1 -1
  100. package/dist/assets/stateDiagram-D77RDMKH-HSEurT6h-CjaTsaSj.js +1 -0
  101. package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-D5M2uKWS.js +1 -0
  102. package/dist/assets/{svg-CaHU_sxA-Z3rOtT_R.js → svg-D_RYJvQC-BQaZtIwL.js} +1 -1
  103. package/dist/assets/swimlanes-42K2YHIH-umBsuvsI-Cteyd1zf.js +1 -0
  104. package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-BhIkbFNE.js +8 -0
  105. package/dist/assets/{tex-B1cONMvb-DcRnJGdj.js → tex-Dx2UJ3VU-a2I8vxmO.js} +1 -1
  106. package/dist/assets/{timeline-definition-24CTP7MA-Bx3KcICO-DL8QwA1A.js → timeline-definition-24CTP7MA-D9plt22Y-X-qCSaCR.js} +2 -2
  107. package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-B8YUBDwK.js +1 -0
  108. package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-Y5xGTwwF.js +1 -0
  109. package/dist/assets/{vennDiagram-4TSXK5OY-DSexNe_H-BJy1ZzMN.js → vennDiagram-4TSXK5OY-DZe7wY85-DvoJHcDM.js} +1 -1
  110. package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-onMmscYz.js +1 -0
  111. package/dist/assets/{wardleyDiagram-VM6X3IG4-DesYYyZW-CzErQ6vX.js → wardleyDiagram-VM6X3IG4-DK9yytWx-ophsOvNL.js} +3 -3
  112. package/dist/assets/{xychartDiagram-S5SC5T6Z-dIt6pHWV-B9Deu8_-.js → xychartDiagram-S5SC5T6Z-CLSO4Pc9-AcfYTBMz.js} +3 -3
  113. package/dist/index.html +1 -1
  114. package/package.json +15 -15
  115. package/server/agents/antigravity-mcp.ts +7 -39
  116. package/server/agents/copilot-hooks-file.ts +8 -31
  117. package/server/agents/copilot-sessions.ts +50 -22
  118. package/server/agents/cursor-hooks-file.ts +9 -28
  119. package/server/agents/cursor-mcp.ts +9 -35
  120. package/server/agents/mcp-config-file.ts +55 -0
  121. package/server/agents/muse-session.ts +3 -34
  122. package/server/agents/owned-file.ts +68 -0
  123. package/server/agents/sqlite-read.ts +38 -0
  124. package/server/backends/presentPathRoot.ts +2 -2
  125. package/server/config/agent-from-argv.ts +28 -0
  126. package/server/config/app-config.ts +14 -0
  127. package/server/config/config-routes.ts +15 -3
  128. package/server/config/env.ts +10 -0
  129. package/server/files/open-dir.ts +10 -51
  130. package/server/files/spawnOpener.ts +23 -2
  131. package/server/index.ts +4 -1
  132. package/server/infra/host-tools.ts +4 -4
  133. package/server/infra/pty-env.ts +19 -6
  134. package/server/infra/shapescript-manage-tool.ts +196 -0
  135. package/server/routes/plugin-routes.ts +16 -14
  136. package/server/session/transcript-view-codex.ts +150 -0
  137. package/server/session/transcript-view-copilot.ts +76 -0
  138. package/server/session/transcript-view-cursor.ts +153 -0
  139. package/server/session/transcript-view-read.ts +225 -34
  140. package/server/session/transcript-view.ts +62 -18
  141. package/server/skills/mulmoterminal-config/SKILL.md +1 -0
  142. package/server/skills/mulmoterminal-model/SKILL.md +26 -1
  143. package/dist/assets/architecture-7GRP2DOG-CRS69XmP-DpDCM-7V.js +0 -1
  144. package/dist/assets/architectureDiagram-5GKGNRK7-BhcT6qS5-Baq_JrpW.js +0 -36
  145. package/dist/assets/array-CApNbSU9-BifhSqXX.js +0 -1
  146. package/dist/assets/channel-BjESmYnj-Dr25JeaW.js +0 -1
  147. package/dist/assets/chunk-4HAMMTFA-ChwfIAnF-BDdeq7BI.js +0 -62
  148. package/dist/assets/chunk-GMAD6QVW-JTqlimUX-wV8tTz7w.js +0 -13
  149. package/dist/assets/chunk-GVQU2GXP-D6ccB4DO-DSqETOAb.js +0 -1
  150. package/dist/assets/chunk-L3NEJ4N5-CUoAQVG2-CznTknVk.js +0 -1
  151. package/dist/assets/chunk-OSK3NFVY-_VNbXQSu-BOPlkrah.js +0 -10
  152. package/dist/assets/chunk-P2QGCYS3-B8lzOn3Y-GBonRamd.js +0 -1
  153. package/dist/assets/chunk-PWAF6VOD-DzjnfYSw-DlHoeX2q.js +0 -1
  154. package/dist/assets/classDiagram-ZZMXUADV-C7gP-pUH-CPvsl0Ux.js +0 -1
  155. package/dist/assets/classDiagram-v2-VYDZK3BY-CUwV5k1F-CPvsl0Ux.js +0 -1
  156. package/dist/assets/cynefin-OW5HDTMX-C4iKivN4-DdU3BeU3.js +0 -1
  157. package/dist/assets/dagre-GXQ25YYZ-DOztjUR_-v1g1_N9e.js +0 -4
  158. package/dist/assets/eventmodeling-NTZA5JFV-DhqtNS8R-C9p-sqyF.js +0 -1
  159. package/dist/assets/flowDiagram-HODETNUW-CAW_8mwY-C7N_B32G.js +0 -1
  160. package/dist/assets/gitGraph-4MIJSDKK-S_Zs6Nxs-DY1iSFH4.js +0 -1
  161. package/dist/assets/graphlib-I2XjzNHe-RhW8UfaN.js +0 -1
  162. package/dist/assets/info-A6RAGUB7-4sGJh1is-C7ujB-Ie.js +0 -1
  163. package/dist/assets/line-DUb1TjJS-3fYWqbSc.js +0 -1
  164. package/dist/assets/mermaid.core-CZDx2Cyn-C08TJAFj.js +0 -44
  165. package/dist/assets/packet-AYTQ26CC-MQQyx8fI-2fyYIiyC.js +0 -1
  166. package/dist/assets/pie-WAS4IAKB-CyaL5i_d-Cx6D4r8D.js +0 -1
  167. package/dist/assets/radar-RG4KPBEZ-DR-SsHDJ-DvcjCpIE.js +0 -1
  168. package/dist/assets/railroad-74A4TZTK-C2gqsOGX-CZDIxK_E.js +0 -1
  169. package/dist/assets/railroad-abnf-HS5TGJTU-Dpzw6P5u-C1DLl3Zi.js +0 -1
  170. package/dist/assets/railroad-ebnf-LZEXJU2U-J_BuIrPo-CIwsP1_7.js +0 -1
  171. package/dist/assets/railroad-peg-WCYAUIDC-DzMStOR--svRhPIKR.js +0 -1
  172. package/dist/assets/rough.esm-CRiMXkOs-Dy-Kn_BL.js +0 -1
  173. package/dist/assets/stateDiagram-D77RDMKH-DbtoW7HL-4vaZ49Do.js +0 -1
  174. package/dist/assets/stateDiagram-v2-MP3YSRHH-CX6RalKY-R5jhdX-f.js +0 -1
  175. package/dist/assets/swimlanes-42K2YHIH-90h8w_3B-MickVn2k.js +0 -1
  176. package/dist/assets/swimlanesDiagram-VR7AAH4N-49XH_TMr-DC1vETI3.js +0 -8
  177. package/dist/assets/treeView-Q6P3EWNA-gFs854Sn-Dye9sPOg.js +0 -1
  178. package/dist/assets/treemap-WGGIJYW6-DGh_szUA-CiKg4aQq.js +0 -1
  179. package/dist/assets/wardley-WFR3VGLG-BVLYL3gB-BdfK_sE2.js +0 -1
  180. package/server/infra/shapescript-publish-tool.ts +0 -133
  181. /package/dist/assets/{katex-DImtHWbS-ZlcWpGUi.js → katex-DJvMs1Qi-ZlcWpGUi.js} +0 -0
  182. /package/dist/assets/{path-CciMXxPa-fybaL0A-.js → path-D09ilxrb-fybaL0A-.js} +0 -0
package/README.md CHANGED
@@ -58,8 +58,10 @@ ping to your phone when a task finishes. One `npx` command, no Electron, no conf
58
58
  npx mulmoterminal@latest # starts on http://localhost:34567 and opens your browser
59
59
  ```
60
60
 
61
- Requires **Node ≥ 22.12** and the [`claude`](https://claude.com/claude-code) CLI on your
62
- `PATH`, already logged in. `npx mulmoterminal@latest init` reports what it can't find.
61
+ Requires **Node ≥ 22.12**, and by default the [`claude`](https://claude.com/claude-code) CLI —
62
+ on your `PATH` or named by `CLAUDE_BIN`, already logged in. Declare a different default agent
63
+ (`--agent codex`, or `"defaultAgent"` in `~/.mulmoterminal/config.json`) and start-up checks that
64
+ one instead. `npx mulmoterminal@latest init` reports what it can't find.
63
65
 
64
66
  ### Why not tmux + iTerm panes?
65
67
 
@@ -255,7 +257,7 @@ more.
255
257
 
256
258
  ## Install & run
257
259
 
258
- Needs **Node ≥ 22.12**, plus these CLIs on your `PATH`:
260
+ Needs **Node ≥ 22.12**, plus these CLIs — on your `PATH`, or named by the matching `<AGENT>_BIN`:
259
261
 
260
262
  > **Never installed any of this before?** The guide walks it end to end, macOS and Windows,
261
263
  > assuming no command-line experience:
@@ -264,7 +266,7 @@ Needs **Node ≥ 22.12**, plus these CLIs on your `PATH`:
264
266
 
265
267
  | | Tool | What it gives you | Install |
266
268
  | --- | --- | --- | --- |
267
- | **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 |
269
+ | **Required** *(unless you declare another)* | [`claude`](https://claude.com/claude-code) | every Claude session — this app is a cockpit for it. Start-up requires it **by default**; declare a default agent and it checks that one instead — `npx mulmoterminal --agent codex`, or `"defaultAgent": "codex"` in `~/.mulmoterminal/config.json` (#2082). `CLAUDE_BIN` is honoured, so an install off `PATH` counts | `npm i -g @anthropic-ai/claude-code`, then run `claude` once to log in |
268
270
  | **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) |
269
271
  | **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` |
270
272
  | 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`) |
@@ -276,7 +278,10 @@ Needs **Node ≥ 22.12**, plus these CLIs on your `PATH`:
276
278
 
277
279
  The server starts without any of the non-required rows; you just lose that row's feature,
278
280
  and the header/panel for it says so. `git` and `gh` are marked required because losing them
279
- costs whole views rather than one button. `npx mulmoterminal@latest init` (below) reports which of
281
+ costs whole views rather than one button. `claude` is required only until you name a different
282
+ default agent — see
283
+ [Starting without Claude Code](https://receptron.github.io/mulmoterminal/guide/en/agents.html#default-agent)
284
+ · [日本語](https://receptron.github.io/mulmoterminal/guide/ja/agents.html#default-agent). `npx mulmoterminal@latest init` (below) reports which of
280
285
  these it can find.
281
286
 
282
287
  ```bash
@@ -346,8 +351,8 @@ npx mulmoterminal@latest --cwd ./my-project # work in a specific directory
346
351
  ```
347
352
 
348
353
  The published package ships the server (run via `tsx`) plus the pre-built web UI;
349
- `npx mulmoterminal@latest` checks for the `claude` CLI, picks a free port, starts the
350
- server, and opens the browser. For local development from a clone, see
354
+ `npx mulmoterminal@latest` checks for the `claude` CLI — or for whichever agent you set as the
355
+ default — picks a free port, starts the server, and opens the browser. For local development from a clone, see
351
356
  [Running](#running).
352
357
 
353
358
  **Won't start with `ERR_MODULE_NOT_FOUND`?** If a first `npx` run was interrupted, a half-unpacked `~/.npm/_npx/<hash>` cache can remain and a later run fails at startup — a corrupted npx cache, not a bug in the published package.
@@ -650,7 +655,10 @@ detects `tmux` on `PATH` at startup and uses it automatically when present.
650
655
  | Plugins | GUI-protocol Vue plugins (`@mulmoclaude/*`, `@mulmochat-plugin/*`): markdown, form, image, chart, HTML, collection, accounting, mulmoscript (MulmoCast video/slides), google |
651
656
  | Tests | Vitest + @vue/test-utils + jsdom |
652
657
 
653
- Requires **Node ≥ 22.12** (uses `node --env-file-if-exists`) and the `claude` CLI on `PATH`.
658
+ Requires **Node ≥ 22.12** (uses `node --env-file-if-exists`) and, by default, the `claude` CLI —
659
+ found on `PATH` or named by `CLAUDE_BIN`. Declaring a different default agent (`--agent codex`, or
660
+ `"defaultAgent"` in `~/.mulmoterminal/config.json`) makes start-up check that agent instead (#2082):
661
+ see [Starting without Claude Code](https://receptron.github.io/mulmoterminal/guide/en/agents.html#default-agent).
654
662
 
655
663
  ---
656
664
 
@@ -0,0 +1,15 @@
1
+ // Types for bin/agent-bins.js — hand-written for the reason every other bin/*.d.ts is: the file is
2
+ // plain JS because it runs before tsx does, and the specs and the server are TypeScript.
3
+ import type { TerminalAgent } from "../common/sessionAgent.js";
4
+
5
+ export interface AgentBinSpec {
6
+ /** The environment variable that overrides the command, e.g. `CLAUDE_BIN`. */
7
+ env: string;
8
+ /** The command as installed. NOT always the agent id — agy and cursor-agent are the commands. */
9
+ bin: string;
10
+ }
11
+
12
+ export declare const AGENT_BIN_SPEC: Record<TerminalAgent, AgentBinSpec>;
13
+ export declare function agentBin(agent: string, env?: Record<string, string | undefined>): string | null;
14
+ /** Only the agents this repository documents an install command for. */
15
+ export declare const AGENT_INSTALL_HINT: Partial<Record<TerminalAgent, string>>;
@@ -0,0 +1,38 @@
1
+ // Which binary each hosted agent runs, and the environment variable that overrides it.
2
+ //
3
+ // Here in `bin/` rather than in `server/` because the CLI needs it BEFORE the server exists: the
4
+ // start-up gate has to decide whether the declared agent is installed, and it runs under plain node
5
+ // with no tsx. The server imports from `bin/` already (bin/instances.js), so this direction is the
6
+ // established one.
7
+ //
8
+ // The table is duplicated knowledge by construction — `server/config/agent-bins.ts` builds the same
9
+ // mapping out of the adapters — so a spec pins the two together rather than a comment asking the
10
+ // next person to keep them in sync.
11
+ //
12
+ // The KEY is the agent id and the `bin` is NOT always the same word: agy and cursor-agent are the
13
+ // commands, `antigravity` and `cursor` are what the rest of the app calls them.
14
+ export const AGENT_BIN_SPEC = {
15
+ claude: { env: "CLAUDE_BIN", bin: "claude" },
16
+ codex: { env: "CODEX_BIN", bin: "codex" },
17
+ antigravity: { env: "ANTIGRAVITY_BIN", bin: "agy" },
18
+ grok: { env: "GROK_BIN", bin: "grok" },
19
+ muse: { env: "MUSE_BIN", bin: "muse" },
20
+ copilot: { env: "COPILOT_BIN", bin: "copilot" },
21
+ cursor: { env: "CURSOR_BIN", bin: "cursor-agent" },
22
+ };
23
+
24
+ /** The command that actually runs for `agent`, honouring its `<AGENT>_BIN` override. */
25
+ export function agentBin(agent, env = process.env) {
26
+ const spec = AGENT_BIN_SPEC[agent];
27
+ if (spec === undefined) return null;
28
+ return env[spec.env] || spec.bin;
29
+ }
30
+
31
+ /** The install command for the agents this repository actually documents one for — Claude Code in
32
+ * the start-up gate's own message, codex in PATH_TOOLS. The other five are deliberately ABSENT
33
+ * rather than guessed: an install line that is wrong is worse than none, because it is the one
34
+ * thing a stuck user will paste. They get the binary name and the guide link instead. */
35
+ export const AGENT_INSTALL_HINT = {
36
+ claude: "npm install -g @anthropic-ai/claude-code && claude auth login",
37
+ codex: "npm install -g @openai/codex",
38
+ };
package/bin/cli-args.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare function saysYes(answer: unknown): boolean;
16
16
  export declare const SECOND_INSTANCE_NOTE: string;
17
17
  export declare const MIN_NODE_LABEL: string;
18
18
  export declare function nodeMeetsMinimum(version: string): boolean;
19
- export declare function serverNodeArgs(serverEntry: string, launchDir: string, port: number): string[];
19
+ export declare function serverNodeArgs(serverEntry: string, launchDir: string, port: number, declaredAgent?: string | null): string[];
20
20
  export declare function serverSpawnEnv(env: Record<string, string | undefined>, cwd: string): Record<string, string | undefined>;
21
21
  export interface RunningInstance {
22
22
  pid: number;
package/bin/cli-args.js CHANGED
@@ -426,9 +426,14 @@ export function nodeMeetsMinimum(version) {
426
426
  * MulmoTerminal's own port (#1857). Renaming it would only move that: `MULMOTERMINAL_PORT` is
427
427
  * deliberately given to PTYs (server/session/mcp-config.ts) for the MCP URLs, so a server
428
428
  * reading it as its bind port would clash with itself the moment `yarn dev` ran inside a cell.
429
+ *
430
+ * `--agent` rides the same channel for the same reason (#2082): it is a preference, and a
431
+ * preference in the environment would reach every terminal in every cell exactly as the port did.
432
+ * Omitted entirely when nothing was declared, so the server falls through to its config file.
429
433
  */
430
- export function serverNodeArgs(serverEntry, launchDir, port) {
431
- return ["--import", "tsx", `--env-file-if-exists=${join(launchDir, ".env")}`, serverEntry, "--port", String(port)];
434
+ export function serverNodeArgs(serverEntry, launchDir, port, declaredAgent = null) {
435
+ const base = ["--import", "tsx", `--env-file-if-exists=${join(launchDir, ".env")}`, serverEntry, "--port", String(port)];
436
+ return declaredAgent === null ? base : [...base, "--agent", declaredAgent];
432
437
  }
433
438
 
434
439
  /**
@@ -0,0 +1,19 @@
1
+ // Types for bin/default-agent.js. See that file for why the gate works the way it does.
2
+ import type { TerminalAgent } from "../common/sessionAgent.js";
3
+
4
+ export interface StartupGate {
5
+ /** Whose binary start-up must find. */
6
+ agent: TerminalAgent;
7
+ /** Always true today; named so a future "warn instead" cannot be added by accident. */
8
+ required: boolean;
9
+ /** Whether the user asked for this agent, which changes what the failure says. */
10
+ declared: boolean;
11
+ }
12
+
13
+ export declare function isKnownAgent(value: unknown): value is TerminalAgent;
14
+ /** The raw token after `--agent`, `""` for a valueless flag, or null when absent. */
15
+ export declare function parseAgentArg(args: readonly string[]): string | null;
16
+ export declare function resolveDeclaredAgent(input?: { cliAgent?: string | null; configAgent?: string | null }): string | null;
17
+ export declare function gateFor(declaredAgent: string | null): StartupGate;
18
+ export declare function configuredDefaultAgent(parsedConfig: unknown): string | null;
19
+ export declare function missingAgentMessage(gate: StartupGate, bin: string | null): string[];
@@ -0,0 +1,99 @@
1
+ // Which agent the CLI is being asked to treat as the default, and what the start-up gate must
2
+ // therefore check. Pure — no PATH lookup, no filesystem — so every combination is testable
3
+ // without a machine that has or lacks a given CLI.
4
+ //
5
+ // THE RULE (issue #2082): Claude Code stays REQUIRED while nothing is declared, because that is the
6
+ // setup the overwhelming majority of users have and a silent fallback would leave them wondering
7
+ // which agent they are talking to. Declare a default and the gate follows the declaration instead:
8
+ // it stops asking about claude entirely and asks about the agent that was named.
9
+ //
10
+ // WHAT THIS IS NOT: it does not decide what an EXISTING cell runs. On disk and on the wire, an
11
+ // absent `agent` field means claude (src/components/gridTabs.ts says so), and that is a storage
12
+ // format rather than a preference — repointing it at this setting would silently re-launch every
13
+ // saved Claude cell as something else. This value is read when a NEW session starts, and nowhere
14
+ // in the path that restores an old one.
15
+ import { AGENT_BIN_SPEC, AGENT_INSTALL_HINT } from "./agent-bins.js";
16
+
17
+ export const isKnownAgent = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(AGENT_BIN_SPEC, value);
18
+
19
+ /** `--agent <name>`, or `--agent=<name>`. Returns the raw token so the caller can reject an unknown
20
+ * one by name rather than silently ignoring a typo. `null` when the flag is absent; `""` when it
21
+ * is present with nothing after it, which is a usage error rather than "no preference". */
22
+ export function parseAgentArg(args) {
23
+ const joined = args.find((a) => a.startsWith("--agent="));
24
+ if (joined !== undefined) return joined.slice("--agent=".length);
25
+ const i = args.indexOf("--agent");
26
+ if (i === -1) return null;
27
+ return args[i + 1] !== undefined && !args[i + 1].startsWith("-") ? args[i + 1] : "";
28
+ }
29
+
30
+ /**
31
+ * The declared default agent, or null when nothing declares one.
32
+ *
33
+ * Two channels only — the flag and the config file — and the flag wins, which is the ordinary
34
+ * shape: the most local statement of intent beats the most durable one.
35
+ *
36
+ * Deliberately NOT a third, environment channel. The launcher hands its own environment to the
37
+ * server, which hands it to every PTY it spawns, so a variable added here reaches every terminal in
38
+ * every cell — the shape of #955 (NODE_ENV) and #1857 (PORT). `--port` was moved OUT of the
39
+ * environment for that reason (server/config/port-from-argv.ts), and this follows it.
40
+ */
41
+ export function resolveDeclaredAgent({ cliAgent = null, configAgent = null } = {}) {
42
+ const declared = cliAgent ?? configAgent ?? null;
43
+ return declared === null || declared === undefined || declared === "" ? null : declared;
44
+ }
45
+
46
+ /**
47
+ * What the start-up gate should require, given the declaration.
48
+ *
49
+ * `{ agent, required }` — WHICH agent's binary to look for, and whether a miss stops start-up.
50
+ * A declared agent is required for the same reason claude was: the user named it, so a machine
51
+ * without it cannot do what they asked, and saying so beats an empty grid (issue #2082, decision 1).
52
+ */
53
+ export function gateFor(declaredAgent) {
54
+ return declaredAgent === null ? { agent: "claude", required: true, declared: false } : { agent: declaredAgent, required: true, declared: true };
55
+ }
56
+
57
+ /** `defaultAgent` out of a parsed `~/.mulmoterminal/config.json`, or null. Pure: the caller owns the
58
+ * read and the JSON parse, because a config this CLI cannot read must not stop it starting. An
59
+ * unknown name is returned as-is so the caller reports the typo rather than ignoring it. */
60
+ export function configuredDefaultAgent(parsedConfig) {
61
+ if (parsedConfig === null || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) return null;
62
+ const value = Object.prototype.hasOwnProperty.call(parsedConfig, "defaultAgent") ? parsedConfig.defaultAgent : null;
63
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
64
+ }
65
+
66
+ // The SECTION, not the guide index: someone reading this message is stuck on exactly one question,
67
+ // and a link to the front page makes them hunt for the answer they were sent to find.
68
+ const GUIDE = {
69
+ en: "https://receptron.github.io/mulmoterminal/guide/en/agents.html#default-agent",
70
+ ja: "https://receptron.github.io/mulmoterminal/guide/ja/agents.html#default-agent",
71
+ };
72
+
73
+ /**
74
+ * What to print when the gate's agent is not installed.
75
+ *
76
+ * Short on purpose. The rule here surprises people — Claude Code is required, and yet the app hosts
77
+ * seven agents — so the message has to answer "why me?" and "what do I type?" in the terminal, and
78
+ * send the rest to the guide. Everything longer than this belongs on that page (issue #2082).
79
+ */
80
+ export function missingAgentMessage(gate, bin) {
81
+ const lines = [];
82
+ if (gate.declared) {
83
+ lines.push(`${gate.agent} is set as the default agent, but its command "${bin}" was not found.`);
84
+ lines.push(`Install it and make sure "${bin}" is on PATH, or set ${AGENT_BIN_SPEC[gate.agent]?.env ?? "the agent's *_BIN"} to its full path.`);
85
+ } else {
86
+ lines.push(`Claude Code CLI not found (looked for "${bin}").`);
87
+ const hint = AGENT_INSTALL_HINT.claude;
88
+ lines.push(`Install it: ${hint}`);
89
+ lines.push("");
90
+ lines.push("Or, if you use a different agent, declare it and this check is skipped:");
91
+ lines.push(" npx mulmoterminal --agent codex");
92
+ lines.push(' ~/.mulmoterminal/config.json: { "defaultAgent": "codex" }');
93
+ lines.push(` agents: ${Object.keys(AGENT_BIN_SPEC).join(", ")}`);
94
+ }
95
+ lines.push("");
96
+ lines.push(`Details: ${GUIDE.en}`);
97
+ lines.push(`日本語: ${GUIDE.ja}`);
98
+ return lines;
99
+ }
@@ -0,0 +1,9 @@
1
+ export interface CommandProbe {
2
+ isFile: (candidate: string) => boolean;
3
+ isExecutable: (candidate: string) => boolean;
4
+ }
5
+
6
+ export declare function hasCommand(
7
+ cmd: string,
8
+ options?: { platform?: NodeJS.Platform; env?: Record<string, string | undefined>; probe?: CommandProbe },
9
+ ): boolean;
@@ -0,0 +1,120 @@
1
+ // Is this command present and runnable?
2
+ //
3
+ // RESOLVED, never executed. That is the whole design, and it is what makes both of the bugs this
4
+ // file was created for impossible rather than handled:
5
+ //
6
+ // 1. INJECTION. The command can be a `<AGENT>_BIN` the user set (#2082). The original probe ran
7
+ // `execSync(`${cmd} ${arg}`)`, so a shell both SPLIT it and INTERPRETED it — measured:
8
+ // CLAUDE_BIN="/…/My Tools/claude" -> REFUSED, split at the space
9
+ // CLAUDE_BIN="true; touch /tmp/X" -> the `touch` RAN
10
+ // 2. WINDOWS `.cmd`. `execFileSync` cannot launch a `.bat`/`.cmd` at all — node documents it,
11
+ // and node refuses it outright since CVE-2024-27980. An npm-installed `codex` on Windows IS
12
+ // `codex.cmd`, so an argv probe reports it missing on the one platform where the shell probe
13
+ // used to work (Codex round 2 of #2084).
14
+ //
15
+ // A file lookup has neither problem: nothing is interpreted, it is just candidate names on disk.
16
+ // WHICH candidates is the whole remaining question, and it is not this file's to invent — it is
17
+ // `server/infra/has-binary.ts`'s, because "can we launch it" and "what would we launch" must not
18
+ // answer differently. `test/bin/gate-agrees-with-spawn.spec.ts` compares the two over generated
19
+ // machines rather than trusting these comments.
20
+ //
21
+ // The trade, said out loud: this reports a binary that EXISTS and is executable, not one that
22
+ // exits 0. A present-but-broken install now reads as present. That is what the server's own
23
+ // pre-spawn check already concluded, and the alternative is executing a string the user supplied.
24
+ import { accessSync, constants, statSync } from "node:fs";
25
+ import path from "node:path";
26
+
27
+ // The candidate names node-pty can actually START on Windows — `.exe`/`.com` as PE images, and
28
+ // `.cmd`/`.bat` through the cmd.exe wrapper resolve-bin builds. Deliberately NOT the user's
29
+ // PATHEXT: a stock one also carries `.VBS`, `.JS`, `.WSF`, `.MSC`, none of which CreateProcessW
30
+ // will run, so honouring it passed a machine whose every session then died with node-pty's empty
31
+ // `File not found:`. Reading it also cut the list the other way — `PATHEXT=.PS1` hid a real
32
+ // `claude.exe` — which is why the answer is a closed set and not an environment variable.
33
+ //
34
+ // `""` belongs to it for the reason resolve-bin states: node-pty's own pre-spawn lookup compares
35
+ // file names EXACTLY, so an extension-less PE image on PATH is one it finds, and refusing it would
36
+ // refuse a host that spawns fine today.
37
+ const WINDOWS_LAUNCHABLE_EXTENSIONS = ["", ".exe", ".com", ".cmd", ".bat"];
38
+
39
+ /** A name CreateProcess/execvp resolves itself rather than by searching PATH. */
40
+ const namesAPath = (cmd) => cmd.includes("/") || cmd.includes("\\");
41
+
42
+ const realProbe = {
43
+ isFile: (candidate) => {
44
+ try {
45
+ return statSync(candidate).isFile();
46
+ } catch {
47
+ return false;
48
+ }
49
+ },
50
+ // Windows has no execute bit that means anything here — the file TYPE is what decides, and the
51
+ // extension loop has already applied it.
52
+ isExecutable: (candidate) => {
53
+ try {
54
+ accessSync(candidate, constants.X_OK);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ },
60
+ };
61
+
62
+ // "" first so an explicit `foo.exe` is found as itself rather than as `foo.exe.EXE`.
63
+ const extensionsFor = (platform) => (platform === "win32" ? WINDOWS_LAUNCHABLE_EXTENSIONS : [""]);
64
+
65
+ // A Windows PATH entry may be QUOTED — `"C:\Program Files\tools"` — which the shells strip and a
66
+ // plain join would not, leaving a path that matches nothing. Same rule as the server's
67
+ // `windowsSearchDirectories` (server/infra/resolve-bin.ts); `C:\Program Files` is the canonical
68
+ // path that needs the quotes, so this is the common case rather than an exotic one.
69
+ // The current directory is on neither platform's list. cmd.exe searches it and POSIX shells do
70
+ // not, but what launches an agent is node-pty on both — and the directory THIS process sits in is
71
+ // the launch directory, never the one the PTY will run in.
72
+ const searchDirectories = (platform, env) => {
73
+ const raw = env.PATH || env.Path || "";
74
+ if (platform === "win32") {
75
+ return raw
76
+ .split(";")
77
+ .map((entry) => entry.replace(/^"(.*)"$/, "$1"))
78
+ .filter((entry) => entry !== "");
79
+ }
80
+ return raw.split(":").filter((entry) => entry !== "");
81
+ };
82
+
83
+ // Can this process enumerate everything execvp would search? Two cases where it cannot, both taken
84
+ // from server/infra/has-binary.ts, which reasoned them out first:
85
+ //
86
+ // - an UNSET PATH is not an empty one. execvp falls back to its own built-in default
87
+ // (confstr _CS_PATH), which is not readable from here.
88
+ // - a NON-ABSOLUTE entry — `tools`, `.`, `../bin`, and the EMPTY entry, which POSIX reads as the
89
+ // current directory (`PATH=/usr/bin:`, `/a::/b`) — is resolved against the CHILD's working
90
+ // directory, not this process's.
91
+ //
92
+ // This matters because this gate REFUSES START-UP. The repo's own rule for a preflight in that
93
+ // position: one that cannot answer must never be the thing that says no (Codex round 6 of #2084,
94
+ // which measured /bin/sh running ./faux with PATH=":/usr/bin").
95
+ const canEnumeratePosixPath = (env) => env.PATH !== undefined && env.PATH.split(":").every((entry) => entry.startsWith("/"));
96
+
97
+ /**
98
+ * True when `cmd` names something this machine could launch.
99
+ *
100
+ * `platform`, `env` and `probe` are injected so the WINDOWS rules are testable from any host —
101
+ * which matters more than usual here, because the Windows branch is the one that regressed and the
102
+ * one no developer on macOS can exercise (reveal-argv.spec.ts makes the same argument).
103
+ */
104
+ export function hasCommand(cmd, { platform = process.platform, env = process.env, probe = realProbe } = {}) {
105
+ if (typeof cmd !== "string" || cmd === "") return false;
106
+ const runnable = (candidate) => probe.isFile(candidate) && (platform === "win32" || probe.isExecutable(candidate));
107
+ // EXACTLY, with no extension appended. Handed a path, node-pty checks that path and nothing else
108
+ // (CreateProcessW's own `.exe` guess never runs, because the spawn dies at node-pty's check
109
+ // first) — so `CLAUDE_BIN=C:\tools\claude` beside a `claude.exe` is a setup that does not work,
110
+ // and a gate that passed it would only move the failure to the first session.
111
+ if (namesAPath(cmd)) return runnable(cmd);
112
+ const extensions = extensionsFor(platform);
113
+ // Answered BEFORE the search, because the answer is "we cannot tell" rather than "not found".
114
+ if (platform !== "win32" && !canEnumeratePosixPath(env)) return true;
115
+ // The path module has to MATCH the platform being asked about, not the one this process runs on.
116
+ // Without it the Windows branch builds `C:\npm/codex.cmd` when exercised from macOS — which is
117
+ // how the Windows tests here found their own harness bug before they found a real one.
118
+ const join = platform === "win32" ? path.win32.join : path.posix.join;
119
+ return searchDirectories(platform, env).some((dir) => extensions.some((ext) => runnable(join(dir, cmd + ext))));
120
+ }
@@ -6,10 +6,10 @@
6
6
  // runs the server via tsx. Mirrors the mulmoclaude launcher.
7
7
 
8
8
  import { execSync, spawn } from "node:child_process";
9
- import { existsSync, statSync } from "node:fs";
9
+ import { existsSync, readFileSync, statSync } from "node:fs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createServer } from "node:net";
12
- import { release } from "node:os";
12
+ import { homedir, release } from "node:os";
13
13
  import { dirname, join, resolve } from "node:path";
14
14
  import { createInterface } from "node:readline";
15
15
  import { fileURLToPath } from "node:url";
@@ -36,7 +36,10 @@ import {
36
36
  serverNodeArgs,
37
37
  serverSpawnEnv,
38
38
  } from "./cli-args.js";
39
+ import { hasCommand } from "./has-command.js";
39
40
  import { liveInstances } from "./instances.js";
41
+ import { agentBin, AGENT_BIN_SPEC } from "./agent-bins.js";
42
+ import { configuredDefaultAgent, gateFor, isKnownAgent, missingAgentMessage, parseAgentArg, resolveDeclaredAgent } from "./default-agent.js";
40
43
  import { setProcessTitle } from "./process-title.js";
41
44
 
42
45
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -88,47 +91,55 @@ async function checkForUpdate() {
88
91
  }
89
92
  }
90
93
 
91
- // Detect a CLI on the user's PATH by asking for its version. Intentionally resolves from
92
- // PATH — detecting the user's installed tools is the whole point of the pre-flight /
93
- // `init` checks.
94
- function hasCommand(cmd, versionArg = "--version") {
94
+ const CONFIG_FILE = join(homedir(), ".mulmoterminal", "config.json");
95
+ const KNOWN_AGENTS = Object.keys(AGENT_BIN_SPEC);
96
+
97
+ // `defaultAgent` out of the user's config, or null. EVERY failure is null: a config file that is
98
+ // missing, unreadable or malformed must not stop the app starting, because the whole point of this
99
+ // setting is to get people INTO the app. A malformed file is the server's to complain about, where
100
+ // the message can be a real one.
101
+ function readConfiguredDefaultAgent() {
95
102
  try {
96
- execSync(`${cmd} ${versionArg}`, { stdio: "pipe" });
97
- return true;
103
+ return configuredDefaultAgent(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
98
104
  } catch {
99
- return false;
105
+ return null;
100
106
  }
101
107
  }
102
108
 
103
- function claudeInstalled() {
104
- return hasCommand("claude");
109
+ // The command for `agent`, honouring its `<AGENT>_BIN` override.
110
+ //
111
+ // The override is the point rather than a detail: the gate used to look for the literal word
112
+ // `claude` on PATH while the server ran `process.env.CLAUDE_BIN || "claude"`
113
+ // (server/agents/claude.ts). A user who set CLAUDE_BIN to a real install was refused start-up by a
114
+ // check asking a different question than the thing it was gating (#2082).
115
+ function agentInstalled(agent) {
116
+ const bin = agentBin(agent);
117
+ return bin === null ? false : hasCommand(bin);
105
118
  }
106
119
 
107
120
  // PATH tools the app shells out to; mirrors the requirements table in README.md. `required`
108
121
  // ones back the core grid — without them a developer loses whole views rather than one
109
122
  // feature — so a miss is an ✗, not an ○.
110
123
  const PATH_TOOLS = [
111
- { cmd: "git", versionArg: "--version", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
112
- { cmd: "gh", versionArg: "--version", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
124
+ { cmd: "git", required: true, why: "worktrees, per-cell branch/diff, PR footer", hint: "brew install git · apt install git" },
125
+ { cmd: "gh", required: true, why: "PRs & Issues view + one-click PRs", hint: "https://cli.github.com (then: gh auth login)" },
113
126
  // Optional rather than a second required tool: github.com is what the app is built around, and a
114
127
  // user with no GitLab project should not be told they are missing something (#981).
115
128
  {
116
129
  cmd: "glab",
117
- versionArg: "--version",
118
130
  required: false,
119
131
  why: "the PRs & Issues view for gitlab.com projects",
120
132
  hint: "brew install glab (then: glab auth login)",
121
133
  },
122
- { cmd: "tmux", versionArg: "-V", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
123
- { cmd: "codex", versionArg: "--version", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
134
+ { cmd: "tmux", required: false, why: "sessions survive a restart", hint: "brew install tmux · apt install tmux" },
135
+ { cmd: "codex", required: false, why: "run OpenAI Codex as an agent", hint: "npm install -g @openai/codex" },
124
136
  {
125
137
  cmd: "ffmpeg",
126
- versionArg: "-version",
127
138
  required: false,
128
139
  why: "video rendering in the mulmo-script panel",
129
140
  hint: "brew install ffmpeg · apt install ffmpeg",
130
141
  },
131
- { cmd: "ollama", versionArg: "--version", required: false, why: "a fully local model via claude-ollama", hint: "https://ollama.com/download" },
142
+ { cmd: "ollama", required: false, why: "a fully local model via claude-ollama", hint: "https://ollama.com/download" },
132
143
  ];
133
144
 
134
145
  // The variables WSL exports into a login shell, then the kernel that names itself
@@ -150,7 +161,6 @@ function fileDialogTool() {
150
161
  if (isWslHost()) {
151
162
  return {
152
163
  cmd: "powershell.exe",
153
- versionArg: "-NoProfile -Command exit",
154
164
  required: false,
155
165
  why: `${why}, using the Windows dialog`,
156
166
  hint: "enable WSL interop (wsl.conf), or: sudo apt install zenity",
@@ -158,15 +168,14 @@ function fileDialogTool() {
158
168
  }
159
169
  return {
160
170
  cmd: "zenity",
161
- versionArg: "--version",
162
171
  required: false,
163
172
  why: `${why} (kdialog / qarma / yad also work)`,
164
173
  hint: "sudo apt install zenity · sudo dnf install zenity",
165
174
  };
166
175
  }
167
176
 
168
- function toolCheckLine({ cmd, versionArg, required, why, hint }) {
169
- if (hasCommand(cmd, versionArg)) return ` ✓ ${cmd} — ${why}`;
177
+ function toolCheckLine({ cmd, required, why, hint }) {
178
+ if (hasCommand(cmd)) return ` ✓ ${cmd} — ${why}`;
170
179
  const head = required ? ` ✗ ${cmd} — not found, needed for ${why}` : ` ○ ${cmd} — optional (${why})`;
171
180
  return `${head}\n → ${hint}`;
172
181
  }
@@ -181,6 +190,36 @@ function promptYesNo(question) {
181
190
  });
182
191
  }
183
192
 
193
+ // Which agent this machine is being asked to run by default, and therefore which binary has to be
194
+ // there. Claude Code stays REQUIRED while nothing is declared — that is the setup nearly everyone
195
+ // has, and a silent fallback would leave them guessing which agent answered. Declare one and the
196
+ // gate follows the declaration instead (#2082).
197
+ //
198
+ // Exits rather than returning a failure: every branch here is "this machine cannot do what was
199
+ // asked", and the caller has nothing to add. Returns the declaration so the server can be told.
200
+ function passStartupGate(args) {
201
+ const cliAgent = parseAgentArg(args);
202
+ if (cliAgent !== null && !isKnownAgent(cliAgent)) {
203
+ error(cliAgent === "" ? "--agent needs an agent name." : `Unknown agent for --agent: ${cliAgent}`);
204
+ error(`Agents: ${KNOWN_AGENTS.join(", ")}`);
205
+ process.exit(1);
206
+ }
207
+ const configAgent = readConfiguredDefaultAgent();
208
+ if (configAgent !== null && !isKnownAgent(configAgent)) {
209
+ error(`Unknown "defaultAgent" in ${CONFIG_FILE}: ${configAgent}`);
210
+ error(`Agents: ${KNOWN_AGENTS.join(", ")}`);
211
+ process.exit(1);
212
+ }
213
+ const declaredAgent = resolveDeclaredAgent({ cliAgent, configAgent });
214
+ const gate = gateFor(declaredAgent);
215
+ if (!agentInstalled(gate.agent)) {
216
+ missingAgentMessage(gate, agentBin(gate.agent)).forEach((line) => error(line));
217
+ process.exit(1);
218
+ }
219
+ log(gate.declared ? `Default agent: ${gate.agent} ✓` : "Claude Code CLI ✓");
220
+ return declaredAgent;
221
+ }
222
+
184
223
  // `npx mulmoterminal init` — idempotent first-run setup. Environment/CLI checks + the
185
224
  // optional interactive-config launch live here (PATH-command detection); the config
186
225
  // derivation + write is the tsx-run server/cli-init.ts.
@@ -190,7 +229,7 @@ async function runInit(initArgs) {
190
229
  const nodeOk = nodeMeetsMinimum(process.versions.node);
191
230
  console.log(nodeOk ? ` ✓ Node ${process.versions.node}` : ` ✗ Node ${process.versions.node} — MulmoTerminal needs ≥ ${MIN_NODE_LABEL}`);
192
231
 
193
- const hasClaude = claudeInstalled();
232
+ const hasClaude = agentInstalled("claude");
194
233
  if (hasClaude) {
195
234
  console.log(" ✓ Claude Code CLI");
196
235
  } else {
@@ -436,7 +475,8 @@ function announceReady(url, note, noOpen) {
436
475
  // the port was taken at bind time before it became ready — the caller then
437
476
  // reports that and stops. In every other case (clean shutdown, fatal error,
438
477
  // or the server simply running) the process exits with the server's code.
439
- function runServer(port, probedAddress, localhostIsUnambiguous, noOpen, cwd, onChild) {
478
+ function runServer(port, probedAddress, localhostIsUnambiguous, noOpen, launch, onChild) {
479
+ const { cwd, declaredAgent } = launch;
440
480
  return new Promise((resolveExit) => {
441
481
  log(`Starting MulmoTerminal on port ${port}...`);
442
482
  // stderr is piped (and passed through) so a fatal boot error can be inspected once the
@@ -444,7 +484,7 @@ function runServer(port, probedAddress, localhostIsUnambiguous, noOpen, cwd, onC
444
484
  // and without reading stderr the launcher cannot tell that from a real bug.
445
485
  // The .env comes from where the user ran the command — the spawn's own cwd is the
446
486
  // package directory, so serverNodeArgs makes that path absolute (#795).
447
- const server = spawn(process.execPath, serverNodeArgs(SERVER_ENTRY, process.cwd(), port), {
487
+ const server = spawn(process.execPath, serverNodeArgs(SERVER_ENTRY, process.cwd(), port, declaredAgent), {
448
488
  cwd: PKG_DIR,
449
489
  env: serverSpawnEnv(process.env, cwd),
450
490
  // "ipc" is the fourth entry and the reason the readiness check can stop guessing: the
@@ -552,6 +592,10 @@ Commands:
552
592
  Needs a running server
553
593
 
554
594
  Options:
595
+ --agent <id> Which agent a new cell starts as, and the one start-up checks for
596
+ instead of Claude Code (claude, codex, antigravity, grok, muse,
597
+ copilot, cursor). Also settable as "defaultAgent" in
598
+ ~/.mulmoterminal/config.json; this flag wins and is not saved.
555
599
  --cwd <dir> Working directory claude runs in (default: current directory; relative paths allowed)
556
600
  --port <number> Server port (default: ${DEFAULT_PORT}; the PORT environment variable is
557
601
  used when this flag is absent). If it is in use, you are asked
@@ -618,12 +662,7 @@ async function main() {
618
662
 
619
663
  checkForUpdate();
620
664
 
621
- if (!claudeInstalled()) {
622
- error("Claude Code CLI not found.");
623
- error("Install it first: npm install -g @anthropic-ai/claude-code && claude auth login");
624
- process.exit(1);
625
- }
626
- log("Claude Code CLI ✓");
665
+ const declaredAgent = passStartupGate(args);
627
666
 
628
667
  if (!existsSync(SERVER_ENTRY)) {
629
668
  error(`Server entry not found at ${SERVER_ENTRY}`);
@@ -662,7 +701,7 @@ async function main() {
662
701
  // loses this terminal has something to search for (#1820). The server child names itself the
663
702
  // same, so `pkill mulmoterminal` reaches whichever half is found first.
664
703
  setProcessTitle(port);
665
- await runServer(port, probedAddress, localhostIsUnambiguous, noOpen, cwd, (c) => {
704
+ await runServer(port, probedAddress, localhostIsUnambiguous, noOpen, { cwd, declaredAgent }, (c) => {
666
705
  child = c;
667
706
  });
668
707
  error(portInUseMessage(port, portExplicit));