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.
- package/README.md +16 -8
- package/bin/agent-bins.d.ts +15 -0
- package/bin/agent-bins.js +38 -0
- package/bin/cli-args.d.ts +1 -1
- package/bin/cli-args.js +7 -2
- package/bin/default-agent.d.ts +19 -0
- package/bin/default-agent.js +99 -0
- package/bin/has-command.d.ts +9 -0
- package/bin/has-command.js +120 -0
- package/bin/mulmoterminal.js +71 -32
- package/common/defaultAgent.ts +31 -0
- package/common/toolGroups.ts +9 -8
- package/dist/assets/{AllPackages-CF2GkZ5g-Dcf5cDN0.js → AllPackages-B1YCJRYB-D8E2jiS6.js} +1 -1
- package/dist/assets/{BaseConfiguration-CAwZOAki-DUdHJKsk.js → BaseConfiguration-B6s_tKOW-2eCrGC8V.js} +1 -1
- package/dist/assets/{Element-BMI7Nj5w-hV6e9MWX.js → Element-BMI7Nj5w-Cb2Ekmhb.js} +1 -1
- package/dist/assets/{Entities-C8p_0jCn-B8iGcgdn.js → Entities-C8p_0jCn-pRbhKHQD.js} +1 -1
- package/dist/assets/{FunctionList-YWk54TCy-BirKP0I_.js → FunctionList-YWk54TCy-BzSIvZ-T.js} +1 -1
- package/dist/assets/{InputJax-C5dj-ntL-Darzu8r-.js → InputJax-C5dj-ntL-JrwIzEIc.js} +1 -1
- package/dist/assets/{MathItem-YJvQL-XJ-BelhLvBn.js → MathItem-YJvQL-XJ-BschBN6g.js} +1 -1
- package/dist/assets/{MmlFactory-DuXYIgx8-djfJFX6f.js → MmlFactory-DSoKkE7s-Cvf26hUZ.js} +1 -1
- package/dist/assets/{Options-oARQeeuT-J_ILSCvw.js → Options-oARQeeuT-oG2rsyL_.js} +1 -1
- package/dist/assets/{OutputJax-DfUZNNCu-DtuXVd03.js → OutputJax-DfUZNNCu-BHj0ddTi.js} +1 -1
- package/dist/assets/{PrioritizedList-6lun1Xqd-BilCpUSB.js → PrioritizedList-6lun1Xqd-8AcZMMAd.js} +1 -1
- package/dist/assets/{Styles-BPXeoqzi-CdLkCiVG.js → Styles-5NmwS-3M-BcRajaa1.js} +1 -1
- package/dist/assets/{TeXAtom-LHT3KFWH-BXMHOoT3.js → TeXAtom-B5ni9myz-Dbw_2bQk.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-DoYNjdBB-B7ZIBaJ6.js → abnfDiagram-VCTEODGH-BGhi-oVb-Dki-UFbe.js} +1 -1
- package/dist/assets/{arc-CqByPKkF-FwfFFIr8.js → arc-CK5ru50N-BytqUmIH.js} +1 -1
- package/dist/assets/architecture-7GRP2DOG-otSkCkT5-BHw2BeHi.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-ELuPYMlf-Bkbt65bl.js +36 -0
- package/dist/assets/{assistive-mml-DXJXho4o-rl36c5mB.js → assistive-mml-CdU3rUpx-DXMsoK-e.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-1Cng0Q4K-_Juv9OUQ.js → blockDiagram-I7D4REHJ-BZ7-OGWQ-BUcazgSJ.js} +4 -4
- package/dist/assets/{c4Diagram-7LVT6UL2-CAb9c8nc-Bd9Kox9w.js → c4Diagram-7LVT6UL2-DCovPZ_4-CdSZg8V0.js} +1 -1
- package/dist/assets/channel-Bmu_OOc6-D6U8MyiH.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DqpzLmar-VEBaVttG.js +74 -0
- package/dist/assets/{chunk-75Z2AOVW-CbPA0Zt_-DVkb-raF.js → chunk-75Z2AOVW-CT5TYzp5-DV0JLVcu.js} +1 -1
- package/dist/assets/{chunk-DU6HZSFF-DuOx2LFX-DQp1Pge9.js → chunk-DU6HZSFF-BVf-rtdf-DZFdIv0b.js} +2 -2
- package/dist/assets/{chunk-F27PBJKO-CgBMw9yN-D7wnwed7.js → chunk-F27PBJKO-rP8pUsk3-BX--T-KN.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-ij1DSzMb-dYKuj_-S.js → chunk-IMKFNOWR-D_IT0r3G-DjS5gJyn.js} +2 -2
- package/dist/assets/{chunk-POPQ4Y6H-Bh_uOTmp-YP4Y0H7o.js → chunk-POPQ4Y6H-C5YURnHe-DkV_Qv-z.js} +1 -1
- package/dist/assets/{chunk-SHT3W25Y-B8dQLS5w-BYPNRBhs.js → chunk-SHT3W25Y-CfF0J8Qh-CqtkI0h1.js} +5 -5
- package/dist/assets/{chunk-SVP7TREG-B6A-T8Th-BYSVtNuZ.js → chunk-SVP7TREG-CTZbRIbX-CB9zieez.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-C1kwD29l-Br0bNia0.js → chunk-TICWLB2K-DP1lYmZm-No0ln892.js} +4 -4
- package/dist/assets/{chunk-XXDRQBXY-vg0kfC73-CPErv3MQ.js → chunk-XXDRQBXY-vg0kfC73--hFAc4CO.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BDQOAOBf-CJ40hHeh.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-CwPXtzoF-CJ40hHeh.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-NuHCtE3P-h2_ltbMI.js → cose-bilkent-JH36ORCC-qIiD3RGD-KWysQw-T.js} +1 -1
- package/dist/assets/cynefin-OW5HDTMX-Xr6IPdZG-D9cR0g5E.js +1 -0
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-CA7sro34-DEmjN8U0.js → cynefinDiagram-5FMLGOSQ-BZrUCS-v-DBYfaO6r.js} +1 -1
- package/dist/assets/{cytoscape.esm-DuKOKbur-Cp2HF8G-.js → cytoscape.esm-Cu7uu3aJ-NgwPT4ez.js} +2 -2
- package/dist/assets/{dagre-B42rJlko-6iqQ6GWy.js → dagre-C2SZ8h4o-C9DP78vN.js} +1 -1
- package/dist/assets/dagre-GXQ25YYZ-CQcLM4na-Qand5Jrz.js +4 -0
- package/dist/assets/{diagram-S7CK7UJ4-D_nsLfRT-CG7iqcK9.js → diagram-S7CK7UJ4-BdmcmUQh-9BXoKyF6.js} +3 -3
- package/dist/assets/{diagram-UQ7AKVKN-DNN-YX9a-DPhn9r3f.js → diagram-UQ7AKVKN-BtvmEiYl-CumMQ6Hh.js} +2 -2
- package/dist/assets/{diagram-VSXAHHWV--HENzo-l-DiD97poT.js → diagram-VSXAHHWV-qEG6N0bV-OspSlthq.js} +2 -2
- package/dist/assets/{diagram-VX7I27RA-m2m9dROj-BKDeFJgq.js → diagram-VX7I27RA-BA8is49N-GNnFbfy3.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-D4qpv4-B-D0DcEN8W.js → diagram-Z3DM3KII-XPcU7IOQ-C1Y1DeOE.js} +1 -1
- package/dist/assets/{dist-D1NuNlw7.js → dist-BFnpugto.js} +1 -1
- package/dist/assets/{dist-CyJfj5nA.js → dist-BWpvwH-7.js} +1 -1
- package/dist/assets/{dist-csgyobRf.js → dist-D9xZ1r8x.js} +1 -1
- package/dist/assets/{dist-DeiyD9Ki-CsdnEslR.js → dist-DeiyD9Ki-Da08vZiU.js} +1 -1
- package/dist/assets/{dist-iwCSUG0R.js → dist-WMBnnmLU.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CAbv3sZg-zYV7b2Vy.js → ebnfDiagram-PWID7BFC-CTHIHdBS-Beitg6Sa.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-a10Ryh0--Dq--2XRb.js → erDiagram-RLTQ6QDP-DVpWTNZj-CWPSP83Q.js} +2 -2
- package/dist/assets/eventmodeling-NTZA5JFV-B7bRrpt_-UhYn-qO9.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-r-3T5g30-DFUJxE9K.js +1 -0
- package/dist/assets/{ganttDiagram-EL5Y4UJY-Cb7leeOC-BZul1n-G.js → ganttDiagram-EL5Y4UJY-BfAgk15F-DDG7lxNr.js} +1 -1
- package/dist/assets/gitGraph-4MIJSDKK-BhzSx5wq-Gt9CfyXY.js +1 -0
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-BCSbUpXG-Cd562rpn.js → gitGraphDiagram-WWUBYQGX-DEVVZ6wt-CkuBQcFO.js} +2 -2
- package/dist/assets/graphlib-BYLhHyYO-BDVf31O9.js +10 -0
- package/dist/assets/{html-DdQiKDqM-BsuwriGq.js → html-Z7dGbczU-cVi1MxGm.js} +1 -1
- package/dist/assets/{index-CDUg8mhn.js → index-CcvdOYTa.js} +381 -316
- package/dist/assets/info-A6RAGUB7-fR2SUJmJ-BW_9XUKB.js +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-BJRaZItN-Cm4T5Wwv.js → infoDiagram-27XIBGKW-CvHUhAHd-2jeed3SU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-DHlFe__S-DGs6xnNx.js → ishikawaDiagram-5VMMS53U-CeqyKdwk-CX0dJEYY.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-BOIo3JM--Bzc0IZn6.js → journeyDiagram-3NMN7TZE-Jfi7MPDi-GFbmO8nm.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-yrQdG683-Cct0Djrh.js → kanban-definition-UXKFOSKX-yOrlojns-BCGoM3TG.js} +4 -4
- package/dist/assets/{lengths-CvmPfBid-CzTSmCcr.js → lengths-CvmPfBid-UAX96cr7.js} +1 -1
- package/dist/assets/{linear-xsuGWu5q-CfNm2yv7.js → linear-jdmEy-qs-DH6G09Ig.js} +1 -1
- package/dist/assets/{liteAdaptor-C0FQiOEx-CTVB31Rv.js → liteAdaptor-pH5MXTG0-4ZCxV5Gl.js} +1 -1
- package/dist/assets/{mathjax-DVEo4UYj-Bp47CElr.js → mathjax-DVEo4UYj-pvH0Vaps.js} +1 -1
- package/dist/assets/{mermaid-parser.core-fkjeiYO7-P9xXkjhl.js → mermaid-parser.core-CzBW8Fof-BkJJkz2V.js} +5 -5
- package/dist/assets/mermaid.core-Dv4uylek-Cb3UvAUX.js +44 -0
- package/dist/assets/{mindmap-definition-YA3MSWOX-BpKAVyqs-d4jTnjxk.js → mindmap-definition-YA3MSWOX-DP_2R9Ga-CgqlDoWp.js} +2 -2
- package/dist/assets/{mo-CrlNnGXX-DC4IkWbc.js → mo-Xv_6CC7A-GIRpAipl.js} +1 -1
- package/dist/assets/packet-AYTQ26CC-B_oKR9KZ-BFLakyCh.js +1 -0
- package/dist/assets/{pegDiagram-XKGWAZYB-C7eG_Ohl-Bq1D--Oy.js → pegDiagram-XKGWAZYB-BsBWryhi-Cao0_mmw.js} +1 -1
- package/dist/assets/pie-WAS4IAKB-D1ZXrN1W-Die2SLux.js +1 -0
- package/dist/assets/{pieDiagram-E7YTZNPT-5f9wSfDR-DSt7pK7-.js → pieDiagram-E7YTZNPT-OugkS2pU-zbbK17Bn.js} +2 -2
- package/dist/assets/{quadrantDiagram-AXDQQJYC-mWeaOj5S-C-jM9gYY.js → quadrantDiagram-AXDQQJYC-DXEAAaUE-CtTU3K_Q.js} +1 -1
- package/dist/assets/radar-RG4KPBEZ-BmUgDkiH-67POH3H6.js +1 -0
- package/dist/assets/railroad-74A4TZTK-6lEXVJo8-CFP2Gi-j.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-BG28Qiz1-y9Rrt74M.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-DzpHhtTH-66aczMob.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B2CHCTyG-BwmZB9OP.js +1 -0
- package/dist/assets/{railroadDiagram-O6MQD6OU-DJskhE7u-myONQWUy.js → railroadDiagram-O6MQD6OU-BJys8O96-BIdHfmqT.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-B5E0bU4h-D9fqLodu.js → requirementDiagram-BXWQKSXE-BaDgCtM5-M_Bk_Guz.js} +2 -2
- package/dist/assets/{sankeyDiagram-P5KCCOFB-DS3AOTai-BthgK6rY.js → sankeyDiagram-P5KCCOFB-puZsr_vK-Qb3kiw8c.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bn2L9osv-DS7EWkDd.js → sequenceDiagram-WJ2MYXX4-D1ziEryC-BHonheSF.js} +6 -6
- package/dist/assets/{src-CLC0ddSd-BdwptT7W.js → src-CLC0ddSd-lXqARcUf.js} +1 -1
- package/dist/assets/stateDiagram-D77RDMKH-HSEurT6h-CjaTsaSj.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DnOa-Otc-D5M2uKWS.js +1 -0
- package/dist/assets/{svg-CaHU_sxA-Z3rOtT_R.js → svg-D_RYJvQC-BQaZtIwL.js} +1 -1
- package/dist/assets/swimlanes-42K2YHIH-umBsuvsI-Cteyd1zf.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-ZLUyNmnU-BhIkbFNE.js +8 -0
- package/dist/assets/{tex-B1cONMvb-DcRnJGdj.js → tex-Dx2UJ3VU-a2I8vxmO.js} +1 -1
- package/dist/assets/{timeline-definition-24CTP7MA-Bx3KcICO-DL8QwA1A.js → timeline-definition-24CTP7MA-D9plt22Y-X-qCSaCR.js} +2 -2
- package/dist/assets/treeView-Q6P3EWNA-BdvZ0_C5-B8YUBDwK.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-CIJLPQlC-Y5xGTwwF.js +1 -0
- package/dist/assets/{vennDiagram-4TSXK5OY-DSexNe_H-BJy1ZzMN.js → vennDiagram-4TSXK5OY-DZe7wY85-DvoJHcDM.js} +1 -1
- package/dist/assets/wardley-WFR3VGLG-CKqtFD8V-onMmscYz.js +1 -0
- package/dist/assets/{wardleyDiagram-VM6X3IG4-DesYYyZW-CzErQ6vX.js → wardleyDiagram-VM6X3IG4-DK9yytWx-ophsOvNL.js} +3 -3
- package/dist/assets/{xychartDiagram-S5SC5T6Z-dIt6pHWV-B9Deu8_-.js → xychartDiagram-S5SC5T6Z-CLSO4Pc9-AcfYTBMz.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +15 -15
- package/server/agents/antigravity-mcp.ts +7 -39
- package/server/agents/copilot-hooks-file.ts +8 -31
- package/server/agents/copilot-sessions.ts +50 -22
- package/server/agents/cursor-hooks-file.ts +9 -28
- package/server/agents/cursor-mcp.ts +9 -35
- package/server/agents/mcp-config-file.ts +55 -0
- package/server/agents/muse-session.ts +3 -34
- package/server/agents/owned-file.ts +68 -0
- package/server/agents/sqlite-read.ts +38 -0
- package/server/backends/presentPathRoot.ts +2 -2
- package/server/config/agent-from-argv.ts +28 -0
- package/server/config/app-config.ts +14 -0
- package/server/config/config-routes.ts +15 -3
- package/server/config/env.ts +10 -0
- package/server/files/open-dir.ts +10 -51
- package/server/files/spawnOpener.ts +23 -2
- package/server/index.ts +4 -1
- package/server/infra/host-tools.ts +4 -4
- package/server/infra/pty-env.ts +19 -6
- package/server/infra/shapescript-manage-tool.ts +196 -0
- package/server/routes/plugin-routes.ts +16 -14
- package/server/session/transcript-view-codex.ts +150 -0
- package/server/session/transcript-view-copilot.ts +76 -0
- package/server/session/transcript-view-cursor.ts +153 -0
- package/server/session/transcript-view-read.ts +225 -34
- package/server/session/transcript-view.ts +62 -18
- package/server/skills/mulmoterminal-config/SKILL.md +1 -0
- package/server/skills/mulmoterminal-model/SKILL.md +26 -1
- package/dist/assets/architecture-7GRP2DOG-CRS69XmP-DpDCM-7V.js +0 -1
- package/dist/assets/architectureDiagram-5GKGNRK7-BhcT6qS5-Baq_JrpW.js +0 -36
- package/dist/assets/array-CApNbSU9-BifhSqXX.js +0 -1
- package/dist/assets/channel-BjESmYnj-Dr25JeaW.js +0 -1
- package/dist/assets/chunk-4HAMMTFA-ChwfIAnF-BDdeq7BI.js +0 -62
- package/dist/assets/chunk-GMAD6QVW-JTqlimUX-wV8tTz7w.js +0 -13
- package/dist/assets/chunk-GVQU2GXP-D6ccB4DO-DSqETOAb.js +0 -1
- package/dist/assets/chunk-L3NEJ4N5-CUoAQVG2-CznTknVk.js +0 -1
- package/dist/assets/chunk-OSK3NFVY-_VNbXQSu-BOPlkrah.js +0 -10
- package/dist/assets/chunk-P2QGCYS3-B8lzOn3Y-GBonRamd.js +0 -1
- package/dist/assets/chunk-PWAF6VOD-DzjnfYSw-DlHoeX2q.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-C7gP-pUH-CPvsl0Ux.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-CUwV5k1F-CPvsl0Ux.js +0 -1
- package/dist/assets/cynefin-OW5HDTMX-C4iKivN4-DdU3BeU3.js +0 -1
- package/dist/assets/dagre-GXQ25YYZ-DOztjUR_-v1g1_N9e.js +0 -4
- package/dist/assets/eventmodeling-NTZA5JFV-DhqtNS8R-C9p-sqyF.js +0 -1
- package/dist/assets/flowDiagram-HODETNUW-CAW_8mwY-C7N_B32G.js +0 -1
- package/dist/assets/gitGraph-4MIJSDKK-S_Zs6Nxs-DY1iSFH4.js +0 -1
- package/dist/assets/graphlib-I2XjzNHe-RhW8UfaN.js +0 -1
- package/dist/assets/info-A6RAGUB7-4sGJh1is-C7ujB-Ie.js +0 -1
- package/dist/assets/line-DUb1TjJS-3fYWqbSc.js +0 -1
- package/dist/assets/mermaid.core-CZDx2Cyn-C08TJAFj.js +0 -44
- package/dist/assets/packet-AYTQ26CC-MQQyx8fI-2fyYIiyC.js +0 -1
- package/dist/assets/pie-WAS4IAKB-CyaL5i_d-Cx6D4r8D.js +0 -1
- package/dist/assets/radar-RG4KPBEZ-DR-SsHDJ-DvcjCpIE.js +0 -1
- package/dist/assets/railroad-74A4TZTK-C2gqsOGX-CZDIxK_E.js +0 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-Dpzw6P5u-C1DLl3Zi.js +0 -1
- package/dist/assets/railroad-ebnf-LZEXJU2U-J_BuIrPo-CIwsP1_7.js +0 -1
- package/dist/assets/railroad-peg-WCYAUIDC-DzMStOR--svRhPIKR.js +0 -1
- package/dist/assets/rough.esm-CRiMXkOs-Dy-Kn_BL.js +0 -1
- package/dist/assets/stateDiagram-D77RDMKH-DbtoW7HL-4vaZ49Do.js +0 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-CX6RalKY-R5jhdX-f.js +0 -1
- package/dist/assets/swimlanes-42K2YHIH-90h8w_3B-MickVn2k.js +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-49XH_TMr-DC1vETI3.js +0 -8
- package/dist/assets/treeView-Q6P3EWNA-gFs854Sn-Dye9sPOg.js +0 -1
- package/dist/assets/treemap-WGGIJYW6-DGh_szUA-CiKg4aQq.js +0 -1
- package/dist/assets/wardley-WFR3VGLG-BVLYL3gB-BdfK_sE2.js +0 -1
- package/server/infra/shapescript-publish-tool.ts +0 -133
- /package/dist/assets/{katex-DImtHWbS-ZlcWpGUi.js → katex-DJvMs1Qi-ZlcWpGUi.js} +0 -0
- /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
|
|
62
|
-
`PATH`, already logged in.
|
|
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. `
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
return true;
|
|
103
|
+
return configuredDefaultAgent(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
|
|
98
104
|
} catch {
|
|
99
|
-
return
|
|
105
|
+
return null;
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
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",
|
|
112
|
-
{ cmd: "gh",
|
|
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",
|
|
123
|
-
{ cmd: "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",
|
|
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,
|
|
169
|
-
if (hasCommand(cmd
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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));
|