mulmoterminal 1.5.0 → 1.7.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 (219) hide show
  1. package/README.md +56 -13
  2. package/bin/claude-ollama.js +183 -0
  3. package/bin/cli-args.d.ts +11 -0
  4. package/bin/cli-args.js +135 -0
  5. package/bin/mulmoterminal.js +71 -88
  6. package/bin/ollama-launch.d.ts +13 -0
  7. package/bin/ollama-launch.js +60 -0
  8. package/bin/update-check.d.ts +21 -0
  9. package/bin/update-check.js +164 -2
  10. package/common/median.ts +13 -0
  11. package/common/modelIds.ts +28 -0
  12. package/common/modelPresets.ts +313 -0
  13. package/dist/assets/{abnfDiagram-VRR7QNED-6nNByj6v-BmTAK_lf.js → abnfDiagram-VRR7QNED-6nNByj6v-BCJQa4DV.js} +1 -1
  14. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-DgJ3pu6_.js +1 -0
  15. package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bgnyaj_i-Rm4oPVV8.js → architectureDiagram-ZJ3FMSHR-Bgnyaj_i-D_Nt7RIE.js} +1 -1
  16. package/dist/assets/{blockDiagram-677ZJIJ3-DQ35o5E4-CpSusKj_.js → blockDiagram-677ZJIJ3-DQ35o5E4-BILj_7MF.js} +1 -1
  17. package/dist/assets/{c4Diagram-LMCZKHZV-ClWZeiWo-CDPWcIZ7.js → c4Diagram-LMCZKHZV-ClWZeiWo-CUaWnhEC.js} +1 -1
  18. package/dist/assets/channel-Di5rtkx0-Bi7KHChX.js +1 -0
  19. package/dist/assets/{chunk-32BRIVSS-CCt9wtYd-BNsLemkR.js → chunk-32BRIVSS-CCt9wtYd-CH_vt2Gl.js} +1 -1
  20. package/dist/assets/{chunk-52WLFC77-FbBbR4uI-hDAV5e1Z.js → chunk-52WLFC77-FbBbR4uI-DyZrk4KQ.js} +1 -1
  21. package/dist/assets/{chunk-C7G6YPKG-C87hlS9c-DfzZj0PL.js → chunk-C7G6YPKG-C87hlS9c-CFx1Arp1.js} +1 -1
  22. package/dist/assets/{chunk-EX3LRPZG-BqGqMXLN-uoc2Ztzj.js → chunk-EX3LRPZG-BqGqMXLN-CiSc3YI0.js} +1 -1
  23. package/dist/assets/{chunk-FWX5IMBZ-BkbSAAuW-B397K7iE.js → chunk-FWX5IMBZ-BkbSAAuW-B6Frv5LB.js} +2 -2
  24. package/dist/assets/{chunk-HOUHSVGY-Cxu0eDlh-BgHFcMoQ.js → chunk-HOUHSVGY-Cxu0eDlh-PsDgJqDO.js} +1 -1
  25. package/dist/assets/{chunk-ICXQ74PX-hiraF_Xj-BJT-y8zi.js → chunk-ICXQ74PX-hiraF_Xj-DYPMB6hF.js} +1 -1
  26. package/dist/assets/{chunk-MOJQB5TN-CqxshQHA-rfYFgPHl.js → chunk-MOJQB5TN-CqxshQHA-gM_q16Ft.js} +1 -1
  27. package/dist/assets/{chunk-OGEWGWER-CjCr7ceX-DuZ8UI47.js → chunk-OGEWGWER-CjCr7ceX-9qc1rmAh.js} +1 -1
  28. package/dist/assets/{chunk-PUDLZKDR-Dx6M-vz1-C20tVRvs.js → chunk-PUDLZKDR-Dx6M-vz1-CbyfAYMR.js} +1 -1
  29. package/dist/assets/{chunk-Q4XR5HBZ-CZd-9lTB-BFkl1myD.js → chunk-Q4XR5HBZ-CZd-9lTB-CHAmO99-.js} +1 -1
  30. package/dist/assets/{chunk-V7JOEXUC-rI0xlC_O-C2QrFL3o.js → chunk-V7JOEXUC-rI0xlC_O-CpbdluPv.js} +1 -1
  31. package/dist/assets/{chunk-VAUOI2AC-DrcykVNK-BSJoRdEM.js → chunk-VAUOI2AC-DrcykVNK-DFpod4Mi.js} +1 -1
  32. package/dist/assets/{chunk-VR4S4FIN-O6iF8Yvf-CL4V93u8.js → chunk-VR4S4FIN-O6iF8Yvf-BDT221yg.js} +1 -1
  33. package/dist/assets/{chunk-WYO6CB5R-B83L_z6I-ybwRHaqV.js → chunk-WYO6CB5R-B83L_z6I-Djv0Pedo.js} +1 -1
  34. package/dist/assets/{chunk-ZGVPDNZ5-BpFv9JSP-DQGEhX3k.js → chunk-ZGVPDNZ5-BpFv9JSP-X8DR2YEI.js} +1 -1
  35. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-B5BBPEcr.js +1 -0
  36. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-B5BBPEcr.js +1 -0
  37. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-CNt3aNde.js +1 -0
  38. package/dist/assets/{cynefinDiagram-TSTJHNR4-x0-0sQ15-B3DfOJaL.js → cynefinDiagram-TSTJHNR4-x0-0sQ15-CDpp4wje.js} +1 -1
  39. package/dist/assets/{dagre-VKFMJZFB-CQdfl-bx-Bf4AA431.js → dagre-VKFMJZFB-CQdfl-bx-MejOSRzt.js} +1 -1
  40. package/dist/assets/{diagram-FQU43EPY-BOSB6VUb-BO7S37GV.js → diagram-FQU43EPY-BOSB6VUb-B55SUPg7.js} +1 -1
  41. package/dist/assets/{diagram-G47NLZAW-DLXrcXsN-Cw4lBqWi.js → diagram-G47NLZAW-DLXrcXsN-BtHyFrEv.js} +1 -1
  42. package/dist/assets/{diagram-NH7WQ7WH-BMQp1rkF-B4OObXRC.js → diagram-NH7WQ7WH-BMQp1rkF-DAo6PIHz.js} +1 -1
  43. package/dist/assets/{diagram-OA4YK3LP-D1wQ0vUj-CbSsGWhY.js → diagram-OA4YK3LP-D1wQ0vUj-BOP-rGWN.js} +1 -1
  44. package/dist/assets/{diagram-WEI45ONY-RR0DpF8R-CkVYiUn6.js → diagram-WEI45ONY-RR0DpF8R-DMkTz0bs.js} +1 -1
  45. package/dist/assets/{ebnfDiagram-CCIWWBDH-M123uVJ8-BIFkvee9.js → ebnfDiagram-CCIWWBDH-M123uVJ8-DYQvurZD.js} +1 -1
  46. package/dist/assets/{erDiagram-Q63AITRT-BWx_-PXG-DhFy59tl.js → erDiagram-Q63AITRT-BWx_-PXG-NRsUZc6C.js} +1 -1
  47. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-DckF6nty.js +1 -0
  48. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-BRAAyPH5.js +1 -0
  49. package/dist/assets/{ganttDiagram-NO4QXBWP-BOoJ1eTw-Cy64oRyD.js → ganttDiagram-NO4QXBWP-BOoJ1eTw-im9hAj69.js} +1 -1
  50. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-C3PN2LIe.js +1 -0
  51. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B2CJhk_G-Do02StAh.js → gitGraphDiagram-IHSO6WYX-B2CJhk_G-BZV7bMgx.js} +1 -1
  52. package/dist/assets/index-Cue6y3NP.css +1 -0
  53. package/dist/assets/index-JiqffS7s.js +617 -0
  54. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-BAo5Fb7g.js +1 -0
  55. package/dist/assets/{infoDiagram-FWYZ7A6U-Mp1X3pBP-CTODYevC.js → infoDiagram-FWYZ7A6U-Mp1X3pBP-n9QeRL84.js} +1 -1
  56. package/dist/assets/{ishikawaDiagram-FXEZZL3T-BNG7tkJu-DWG68Fod.js → ishikawaDiagram-FXEZZL3T-BNG7tkJu-BaCscIGZ.js} +1 -1
  57. package/dist/assets/{journeyDiagram-5HDEW3XC-Dbp_hY9X-C21LZ7jk.js → journeyDiagram-5HDEW3XC-Dbp_hY9X-Dt8UlK_2.js} +1 -1
  58. package/dist/assets/{kanban-definition-HUTT4EX6-DSTc5u3q-ahPtq3pf.js → kanban-definition-HUTT4EX6-DSTc5u3q-DUVHu497.js} +1 -1
  59. package/dist/assets/{lib-Df1jlz1Q.js → lib-1OP8J0D5.js} +1 -1
  60. package/dist/assets/{line-B1wBwzrY-DAl65sYb.js → line-B1wBwzrY-BcbT6-ul.js} +1 -1
  61. package/dist/assets/{marp-D5H57UyA.js → marp-GfniLWYo.js} +1 -1
  62. package/dist/assets/{mermaid-parser.core-DC7NPJ_M-QXgmJCuN.js → mermaid-parser.core-DC7NPJ_M-B3PfPUiA.js} +2 -2
  63. package/dist/assets/{mermaid.core-DZM3Ha-E-B1G-85N2.js → mermaid.core-DZM3Ha-E-BWGnyKvd.js} +3 -3
  64. package/dist/assets/{mindmap-definition-LN4V7U3C-DYtgcMsY-CBnOdPk8.js → mindmap-definition-LN4V7U3C-DYtgcMsY-Bi04e3Dh.js} +1 -1
  65. package/dist/assets/packet-7NZHBO7P-lwb58iYx-B_vqCkVp.js +1 -0
  66. package/dist/assets/{pegDiagram-2B236MQR-C43eIpKM-hEGXt4rK.js → pegDiagram-2B236MQR-C43eIpKM-D5nABd5d.js} +1 -1
  67. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-CAPwLWC2.js +1 -0
  68. package/dist/assets/{pieDiagram-ENE6RG2P-BkTqgJyR-D84c_7gF.js → pieDiagram-ENE6RG2P-BkTqgJyR-BYsX0pmp.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-ABIIQ3AL-Bm1Zjm45-m6ws2UZn.js → quadrantDiagram-ABIIQ3AL-Bm1Zjm45-CpkA29IU.js} +1 -1
  70. package/dist/assets/radar-I7S5WNFK-7CKcb_l--YAwbZSSO.js +1 -0
  71. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-DWNa-BLR.js +1 -0
  72. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--8kHN0m0s.js +1 -0
  73. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-BeQb-Rfm.js +1 -0
  74. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-fqlxInkH.js +1 -0
  75. package/dist/assets/{railroadDiagram-RFXS5EU6-BkfbdeAs-BN3y7GqL.js → railroadDiagram-RFXS5EU6-BkfbdeAs-DljCkwrn.js} +1 -1
  76. package/dist/assets/{requirementDiagram-TGXJPOKE-CrGTTjYg-B0zoTBgF.js → requirementDiagram-TGXJPOKE-CrGTTjYg-C9JLg12J.js} +1 -1
  77. package/dist/assets/{sankeyDiagram-HTMAVEWB-rWXPf03Z-XgG2MUCe.js → sankeyDiagram-HTMAVEWB-rWXPf03Z-DDL7fuaw.js} +1 -1
  78. package/dist/assets/{sequenceDiagram-DBY2YBRQ-nkJYWO2m-BxK5UjRj.js → sequenceDiagram-DBY2YBRQ-nkJYWO2m-Pyu_gxuA.js} +1 -1
  79. package/dist/assets/{stateDiagram-2N3HPSRC-T4-clK8b-Y_DtKuon.js → stateDiagram-2N3HPSRC-T4-clK8b-CYg9qZQS.js} +1 -1
  80. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-DPkjtkgF.js +1 -0
  81. package/dist/assets/{swimlanes-5IMT3BWC-HmQNEntu-D5dAJGJF.js → swimlanes-5IMT3BWC-HmQNEntu-DEKxjcNL.js} +1 -1
  82. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-B2wFtrEU.js +8 -0
  83. package/dist/assets/{timeline-definition-FHXFAJF6-CNc9jSTP-CRCAKMND.js → timeline-definition-FHXFAJF6-CNc9jSTP-DpFI7Gdx.js} +1 -1
  84. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-CxCShMBw.js +1 -0
  85. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-BFi0W9rh.js +1 -0
  86. package/dist/assets/{vennDiagram-L72KCM5P-Dr3pTJ_0-DIFYkt_z.js → vennDiagram-L72KCM5P-Dr3pTJ_0-Dmra9rkk.js} +1 -1
  87. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-2Yl3qxOe.js +1 -0
  88. package/dist/assets/{wardleyDiagram-EHGQE667-DSFc7ZZa-BL_kqA2P.js → wardleyDiagram-EHGQE667-DSFc7ZZa-CbUid2zT.js} +1 -1
  89. package/dist/assets/{xychartDiagram-FW5EYKEG-BP0Nn4Pp-q75mTWUr.js → xychartDiagram-FW5EYKEG-BP0Nn4Pp-BJc0ZRNl.js} +1 -1
  90. package/dist/index.html +2 -2
  91. package/package.json +3 -2
  92. package/server/agents/claude-args.ts +5 -0
  93. package/server/agents/codex-activity.ts +70 -0
  94. package/server/agents/codex-resume.ts +26 -0
  95. package/server/backends/audioAdmission.ts +62 -0
  96. package/server/backends/byte-range.ts +40 -0
  97. package/server/backends/collectionNotifierAdapter.ts +57 -0
  98. package/server/backends/collectionWatchers.ts +9 -49
  99. package/server/backends/collections.ts +22 -18
  100. package/server/backends/docPath.ts +53 -0
  101. package/server/backends/feed-summary.ts +27 -0
  102. package/server/backends/feeds.ts +2 -9
  103. package/server/backends/fileChange.ts +5 -8
  104. package/server/backends/files.ts +10 -52
  105. package/server/backends/image-gen.ts +2 -18
  106. package/server/backends/imageResult.ts +28 -0
  107. package/server/backends/markdown.ts +2 -30
  108. package/server/backends/mutateStatus.ts +28 -0
  109. package/server/backends/rawServingPlan.ts +56 -0
  110. package/server/backends/remoteHost/attachment-path.ts +31 -0
  111. package/server/backends/remoteHost/attachmentStore.ts +1 -24
  112. package/server/backends/remoteHost/handlers.ts +2 -9
  113. package/server/backends/remoteHost/index.ts +5 -12
  114. package/server/backends/remoteHost/onExpire.ts +2 -16
  115. package/server/backends/remoteHost/sessionActivity.ts +11 -1
  116. package/server/backends/remoteHost/stagedStorageIds.spec.ts +38 -0
  117. package/server/backends/remoteHost/stagedStorageIds.ts +16 -0
  118. package/server/backends/remoteView.ts +17 -3
  119. package/server/backends/whisper.ts +6 -53
  120. package/server/config/app-config.ts +29 -1
  121. package/server/config/config-body.ts +27 -0
  122. package/server/config/config-routes.ts +24 -20
  123. package/server/config/config-schema.ts +38 -0
  124. package/server/config/dir-config.ts +9 -0
  125. package/server/config/launch-options.ts +45 -0
  126. package/server/config/update-status.ts +38 -0
  127. package/server/git/branch-query.ts +29 -0
  128. package/server/git/git-parse.ts +34 -0
  129. package/server/git/pr-for-branch.ts +3 -11
  130. package/server/git/prPhase.ts +3 -10
  131. package/server/git/worktree-diff.ts +3 -4
  132. package/server/git/worktree-pr.ts +2 -10
  133. package/server/index.ts +102 -882
  134. package/server/infra/allowed-origin.ts +24 -0
  135. package/server/infra/plugins-registry.ts +20 -9
  136. package/server/infra/process-guards.ts +32 -0
  137. package/server/infra/server-exit.ts +30 -0
  138. package/server/infra/server-tool-load.ts +29 -0
  139. package/server/infra/tmux.ts +21 -1
  140. package/server/infra/tool-precedence.ts +43 -0
  141. package/server/mcp/broker.ts +18 -49
  142. package/server/mcp/tool-envelope.ts +38 -0
  143. package/server/mcp/tool-gate.ts +63 -0
  144. package/server/routes/app-routes.ts +283 -0
  145. package/server/routes/dir-routes.ts +2 -1
  146. package/server/routes/hook-routes.ts +165 -0
  147. package/server/routes/mcp-routes.ts +54 -0
  148. package/server/routes/plugin-narration.ts +18 -0
  149. package/server/routes/plugin-routes.ts +103 -0
  150. package/server/routes/routeParams.ts +19 -0
  151. package/server/routes/session-routes.ts +20 -25
  152. package/server/routes/terminal-ws-path.ts +25 -0
  153. package/server/routes/tool-routes.ts +8 -27
  154. package/server/routes/toolResultPlan.ts +39 -0
  155. package/server/routes/ws-routes.ts +61 -46
  156. package/server/session/activity-flag.ts +31 -0
  157. package/server/session/activity-hook.ts +18 -1
  158. package/server/session/activity-state.ts +17 -0
  159. package/server/session/activity-transition.ts +64 -0
  160. package/server/session/background-chat.ts +47 -0
  161. package/server/session/codex-activity-track.ts +69 -0
  162. package/server/session/codex-activity-watch.ts +51 -0
  163. package/server/session/dev-terminal-sessions.ts +54 -0
  164. package/server/session/draft-injection.ts +4 -4
  165. package/server/session/draft-plan.ts +21 -0
  166. package/server/session/handoff-text.ts +17 -1
  167. package/server/session/header-hook.ts +38 -0
  168. package/server/session/hiddenMarker.ts +15 -0
  169. package/server/session/hook-settings.ts +57 -0
  170. package/server/session/launch-choice.ts +71 -0
  171. package/server/session/lifecycle.ts +209 -0
  172. package/server/session/mcp-config.ts +36 -0
  173. package/server/session/partitionPending.ts +39 -0
  174. package/server/session/provider-env.ts +147 -0
  175. package/server/session/pty-spawn.ts +21 -6
  176. package/server/session/reap-policy.ts +36 -0
  177. package/server/session/registry.ts +60 -19
  178. package/server/session/resumable-sessions.ts +18 -0
  179. package/server/session/scheduled-sessions.ts +9 -0
  180. package/server/session/session-detail-view.ts +54 -0
  181. package/server/session/session-list.ts +8 -6
  182. package/server/session/session-reads.ts +28 -21
  183. package/server/session/session-settings.ts +45 -0
  184. package/server/session/session-title.ts +103 -0
  185. package/server/session/sessionListTitle.ts +22 -0
  186. package/server/session/spawn-claude.ts +55 -23
  187. package/server/session/spawn-codex.ts +19 -2
  188. package/server/session/spawn-deps.ts +7 -2
  189. package/server/session/spawners.ts +15 -0
  190. package/server/session/task-push.ts +54 -0
  191. package/server/session/taskPushRules.ts +33 -0
  192. package/server/session/tool-hook.ts +59 -0
  193. package/server/session/tool-store.ts +63 -15
  194. package/server/session/translation-submit.ts +27 -0
  195. package/server/session/work-phase-tracker.ts +56 -0
  196. package/server/skills/mulmoterminal-config/SKILL.md +58 -2
  197. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-ChEc_7wP.js +0 -1
  198. package/dist/assets/channel-Di5rtkx0-CW5bKrWi.js +0 -1
  199. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-BJBeI7ut.js +0 -1
  200. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-BJBeI7ut.js +0 -1
  201. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-CHrZLRBv.js +0 -1
  202. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-Dows47_h.js +0 -1
  203. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-D8EOvOHn.js +0 -1
  204. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-D0iLvwj3.js +0 -1
  205. package/dist/assets/index-CkheN-4M.css +0 -1
  206. package/dist/assets/index-D7iecJBA.js +0 -607
  207. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-BnqrlbY_.js +0 -1
  208. package/dist/assets/packet-7NZHBO7P-lwb58iYx-vktMe7EJ.js +0 -1
  209. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-DQdd8O0g.js +0 -1
  210. package/dist/assets/radar-I7S5WNFK-7CKcb_l--C24Fc6mu.js +0 -1
  211. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-CZSGevii.js +0 -1
  212. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--CUsXkbxZ.js +0 -1
  213. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-CvkVfCff.js +0 -1
  214. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-DVG8HAZ0.js +0 -1
  215. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-Cehiuymv.js +0 -1
  216. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-CoSHy1jT.js +0 -8
  217. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-CWUjubva.js +0 -1
  218. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-BKgXJcXq.js +0 -1
  219. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-BJhsIgOJ.js +0 -1
package/README.md CHANGED
@@ -12,6 +12,12 @@ ping to your phone when a task finishes. One `npx` command, no Electron, no conf
12
12
  npx mulmoterminal # starts on http://localhost:34567 and opens your browser
13
13
  ```
14
14
 
15
+ - 📖 **User guide:** [English](https://receptron.github.io/mulmoterminal/guide/en/) — the
16
+ grid view, everyday workflows, the full feature list, configuration, and mobile push
17
+ notifications.
18
+ - 📖 **ユーザーガイド:** [日本語](https://receptron.github.io/mulmoterminal/guide/ja/) —
19
+ グリッドの使い方・日々のワークフロー・機能一覧・設定・スマホ通知の設定はこちら。
20
+
15
21
  ![MulmoTerminal's grid view — four live Claude sessions running side by side, each in its own color-coded project](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-2x2-live.png)
16
22
 
17
23
  *The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working / done (blue), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
@@ -21,14 +27,17 @@ npx mulmoterminal # starts on http://localhost:34567 and opens your brows
21
27
  - **See every agent at once.** A grid of live sessions, each cell color-coded by state —
22
28
  **working** (blue), **blocked / needs a permission** (amber), **done, unreviewed** (blue),
23
29
  **idle** — with an attention chime and a toolbar tally, so an off-screen agent that's stuck
24
- never slips past you. Stop babysitting one terminal; supervise ten.
30
+ never slips past you. Stop babysitting one terminal; supervise ten. Zoom into one and the
31
+ **cockpit roster** keeps everyone else in view — one text row per session with its AI
32
+ summary, last prompt, latest reply, and the branch's **PR phase** (draft / CI fail /
33
+ ready / merged).
25
34
  - **A GUI for your agents, not just a terminal.** Beside the terminal, a **Canvas** panel
26
35
  renders what an agent produces over MCP — **documents, forms, charts, generated images,
27
36
  HTML, collection cards** — each drawn by its own plugin. The agent doesn't just print
28
37
  text; it hands you an interface.
29
- - **Get pulled back from anywhere.** A finished background task sends a **Web Push to your
30
- phone**, and the **RemoteHost** companion lets you watch and start sessions from the phone
31
- itself — walk away, get pinged, jump back in.
38
+ - **Get pulled back from anywhere.** A finished or input-waiting — task sends a **Web Push
39
+ to your phone**, and the **RemoteHost** companion lets you watch sessions and answer with a
40
+ tap (**yes / no / continue**) from the phone itself — walk away, get pinged, jump back in.
32
41
  - **Nothing is lost on a restart.** With `tmux`, every session survives a server crash,
33
42
  restart, or `node --watch` reload — a mid-turn agent, a long build, a dev server all keep
34
43
  running and reattach when you come back.
@@ -42,6 +51,12 @@ npx mulmoterminal # starts on http://localhost:34567 and opens your brows
42
51
  `staging` in amber), a configurable header (buttons + info chips), custom attention sounds,
43
52
  and Run / Skill menus to launch a project's scripts and `.claude/skills` right inside a cell.
44
53
 
54
+ ![The cockpit roster — a one-row-per-session summary list beside the enlarged terminal](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/cockpit-roster.png)
55
+
56
+ *Zoomed in, the **cockpit roster** replaces thumbnails with information: every session as a
57
+ text row — directory, AI summary, your last prompt, the agent's latest reply, a status word,
58
+ and the branch's **PR phase** badge. Click a row to swap the enlarged terminal.*
59
+
45
60
  ### What it is, under the hood
46
61
 
47
62
  Each session runs as a real PTY on the server (the agent CLI in a pseudo-terminal) and is
@@ -94,14 +109,28 @@ listener and needs a browser **on the host**. Either way it needs a Desktop OAut
94
109
  as `~/.secrets/client_secret_*.json`; the refresh token lands in `~/.config/mulmo/google-token.json`
95
110
  and is **shared with MulmoClaude**, so one link per machine covers both apps.
96
111
 
112
+ **Local models (optional).** The package also ships `claude-ollama` — a one-command launcher that
113
+ runs Claude Code **fully locally against an [Ollama](https://ollama.com) model** (no cloud, no API
114
+ key). It starts a large-context Ollama server and launches `claude` with a minimal system prompt so
115
+ small models aren't drowned:
116
+
117
+ ```bash
118
+ ollama pull qwen3:4b
119
+ npx -p mulmoterminal claude-ollama qwen3:4b # or, if installed globally: claude-ollama qwen3:4b
120
+ ```
121
+
122
+ See [Local models with claude-ollama](https://receptron.github.io/mulmoterminal/guide/en/claude-ollama.html)
123
+ for the details and model notes.
124
+
97
125
  > **Already linked before the calendar-list / colour features?** They need a read scope your existing
98
126
  > link doesn't have, so `listCalendars` (and, in practice, `colors`) fail with an insufficient-scope
99
127
  > 403 until you re-authorize: **Settings → Google account → Unlink**, then sign in again (or re-run
100
128
  > `google login`). Reading/creating events on your primary calendar keeps working without re-linking.
101
129
 
102
130
  A global install isn't auto-updated, so on startup MulmoTerminal checks npm and
103
- prints a one-line notice when a newer version is available (`npm i -g mulmoterminal`
104
- to update). Disable with `MULMOTERMINAL_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`).
131
+ prints a one-line notice when a newer version is available and the web toolbar shows a
132
+ clickable **update badge** with the exact command for your install (`npm i -g mulmoterminal`,
133
+ or `git pull` for a clone). Disable with `MULMOTERMINAL_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`).
105
134
 
106
135
  Options: `--cwd <dir>` (working directory — relative paths allowed; defaults to the
107
136
  directory you run the command from), `--port <n>` (default 34567), `--no-open`,
@@ -225,6 +254,17 @@ today — **Claude Code** (the default) and **Codex**.
225
254
  cell's launch form and the Collections browser carry a **Claude / Codex** toggle (your
226
255
  choice is remembered).
227
256
 
257
+ **Other models.**
258
+ Claude Code can run against any **Anthropic-compatible** backend (OpenRouter, Moonshot, a
259
+ LiteLLM gateway). Backends are listed in `~/.mulmoterminal/config.json` under `providers`,
260
+ and their **keys are read from the server's environment** — never from a file the app
261
+ serves. A directory sets its default in `.mulmoterminal.json` (`provider` / `model`), and
262
+ each grid cell's launch form has a **MODEL** select that overrides it for one session,
263
+ listing ~27 curated models with the measured pass rate of a real tool-using task beside
264
+ each. A provider whose token can't be resolved **refuses to start** rather than falling
265
+ back to Anthropic, and providers can't be combined with the Docker sandbox. Full walkthrough — setup, the measured model list, adding your own models, troubleshooting:
266
+ [Using another model via OpenRouter](https://receptron.github.io/mulmoterminal/guide/en/providers.html).
267
+
228
268
  **Skills for Codex.** Codex has no `/<slug>` slash commands, so on session setup
229
269
  MulmoTerminal **mirrors the workspace's `.claude/skills` into `~/.codex/skills`** (each
230
270
  mirrored directory carries a `.mt-mirror` marker so a re-sync overwrites what MulmoTerminal
@@ -308,7 +348,7 @@ building the sandbox spawn, so they have no effect unless `MULMOTERMINAL_SANDBOX
308
348
  | -------- | ---------- |
309
349
  | Frontend | Vue 3 (`<script setup>` + TypeScript), Vue Router, Vite, xterm.js (`@xterm/*`), CodeMirror 6, socket.io-client |
310
350
  | Backend | Node (ESM, TypeScript run via `tsx`), Express 5, `ws` (terminal WebSocket), `node-pty`, socket.io, `@modelcontextprotocol/sdk` (in-process GUI MCP) |
311
- | Plugins | GUI-protocol Vue plugins (`@mulmoclaude/*`, `@mulmochat-plugin/*`): markdown, form, image, chart, HTML, collection, accounting |
351
+ | Plugins | GUI-protocol Vue plugins (`@mulmoclaude/*`, `@mulmochat-plugin/*`): markdown, form, image, chart, HTML, collection, accounting, mulmoscript (MulmoCast video/slides), google |
312
352
  | Tests | Vitest + @vue/test-utils + jsdom |
313
353
 
314
354
  Requires **Node ≥ 22.9** (uses `node --env-file-if-exists`) and the `claude` CLI on `PATH`.
@@ -668,8 +708,10 @@ Each grid cell's header shows two badges for its session, refreshed when a turn
668
708
 
669
709
  - **Context badge** — e.g. `Opus · ctx 35%`: the model family plus how full its context
670
710
  window is (the *last* turn's input + cache tokens ÷ the model's window — **1M** for
671
- current-gen Opus / Sonnet / Fable, **200k** otherwise; an unknown model shows the label
672
- with no %).
711
+ current-gen Opus / Sonnet / Fable / Mythos, **200k** otherwise). A session running on a
712
+ [provider model](#agents-claude--codex) shows that model's name and its published window
713
+ (`Kimi K2.7 Code · ctx 12%`); a model in neither list keeps the label and hides the %,
714
+ since the window is never guessed.
673
715
  - **Token badge** — `⇡<in> ⇣<out>`: cumulative input (fresh + cache-read + cache-creation)
674
716
  and output tokens for the session, k/M-formatted, with a full breakdown in the tooltip.
675
717
 
@@ -698,9 +740,9 @@ mirrors the active session, and replays history on re-select. Plugins reach the
698
740
  an **in-process MCP server** served per session at `POST /api/mcp/:sessionId` (server name
699
741
  `mulmoterminal-gui`) — which works from the host *or* the Docker sandbox (over
700
742
  `host.docker.internal`). Which plugins load is gated by `plugins/plugins.json`; the shipped
701
- set includes markdown, form, image generation (needs `GEMINI_API_KEY`), chart, HTML, and
702
- collection views. You can also merge your **own HTTP MCP servers** into the single-view
703
- session via Settings → `userMcpServers`.
743
+ set includes markdown, form, image generation (needs `GEMINI_API_KEY`), chart, HTML,
744
+ collection, and mulmoscript (MulmoCast video/slides/PDF playback) views. You can also merge
745
+ your **own HTTP MCP servers** into the single-view session via Settings → `userMcpServers`.
704
746
 
705
747
  **Wiki.** The toolbar **Wiki** button opens a read-only browser over `<workspace>/data/wiki/`
706
748
  — an **index** (tag-filterable page catalog), rendered **pages** with `[[wiki links]]` and
@@ -938,8 +980,9 @@ same-origin-guarded.
938
980
 
939
981
  | Endpoint | Purpose |
940
982
  | -------- | ------- |
941
- | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`). |
983
+ | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`, `providers`). |
942
984
  | `GET /api/sound` · `/api/dir-sound?cwd=` · `/api/dir-config?cwd=` | Custom / per-directory attention sound + per-dir config. |
985
+ | `GET /api/launch-options` | The Anthropic-compatible backends this server can reach, each with its models and — when it can't — the reason. Reports the **name** of the env var a key is read from, never the key. |
943
986
  | `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
944
987
  | `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
945
988
  | `POST /api/translation` | Runtime UI-string translation. |
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // `claude-ollama <model> [claude args…]` — run Claude Code fully locally against an
4
+ // Ollama-served model. Ollama 0.31+ serves a native Anthropic-compatible `/v1/messages`, so
5
+ // Claude Code connects directly; this launcher just sets up the three things that make it
6
+ // actually work with a small local model:
7
+ // 1. a dedicated large-context `ollama serve` (the 4096 default overflows Claude's prompt),
8
+ // 2. `--bare --disable-slash-commands` so the system prompt is small enough for the model,
9
+ // 3. the env recipe (unset ANTHROPIC_API_KEY, point ANTHROPIC_BASE_URL at the local server).
10
+
11
+ import { execSync, spawn } from "node:child_process";
12
+ import { createServer } from "node:net";
13
+ import { get as httpGet } from "node:http";
14
+ import { parseClaudeOllamaArgs, buildClaudeEnv, buildOllamaServeEnv, buildClaudeArgs, modelIsInstalled, OLLAMA_CONTEXT_LENGTH } from "./ollama-launch.js";
15
+
16
+ const log = (msg) => console.log(`\x1b[36m[claude-ollama]\x1b[0m ${msg}`);
17
+ const error = (msg) => console.error(`\x1b[31m[claude-ollama]\x1b[0m ${msg}`);
18
+
19
+ const READY_TIMEOUT_MS = 20_000;
20
+ const READY_POLL_MS = 400;
21
+
22
+ function printHelp() {
23
+ console.log(
24
+ [
25
+ "Usage: claude-ollama <model> [claude args…]",
26
+ "",
27
+ "Run Claude Code against a local Ollama model — no cloud, no API key.",
28
+ "Starts a dedicated large-context Ollama server on a private port and launches",
29
+ "Claude Code with a minimal system prompt (--bare) so small models aren't drowned.",
30
+ "",
31
+ "Examples:",
32
+ " claude-ollama qwen3:4b",
33
+ ' claude-ollama qwen3:30b-a3b "refactor this module"',
34
+ "",
35
+ "Notes:",
36
+ " - The model must be pulled first: ollama pull <model>",
37
+ " - Validate a model through a real multi-turn tool run; small ones vary.",
38
+ ].join("\n"),
39
+ );
40
+ }
41
+
42
+ function hasCommand(cmd) {
43
+ try {
44
+ execSync(`${cmd} --version`, { stdio: "ignore" });
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // A free TCP port for the private ollama server, so an Ollama the user already runs (11434)
52
+ // is left alone.
53
+ function findFreePort() {
54
+ return new Promise((resolve, reject) => {
55
+ const probe = createServer();
56
+ probe.once("error", reject);
57
+ probe.listen(0, "127.0.0.1", () => {
58
+ const { port } = probe.address();
59
+ probe.close(() => resolve(port));
60
+ });
61
+ });
62
+ }
63
+
64
+ // GET a small JSON endpoint on the local server, best-effort (null on any failure).
65
+ function getJson(port, path, timeout_ms = 2000) {
66
+ return new Promise((resolve) => {
67
+ const req = httpGet({ host: "127.0.0.1", port, path, timeout: timeout_ms }, (res) => {
68
+ let body = "";
69
+ res.on("data", (chunk) => (body += chunk));
70
+ res.on("end", () => {
71
+ try {
72
+ resolve(JSON.parse(body));
73
+ } catch {
74
+ resolve(null);
75
+ }
76
+ });
77
+ });
78
+ req.on("error", () => resolve(null));
79
+ req.on("timeout", () => {
80
+ req.destroy();
81
+ resolve(null);
82
+ });
83
+ });
84
+ }
85
+
86
+ async function waitUntilReady(port) {
87
+ const startedAt = Date.now();
88
+ while (Date.now() - startedAt < READY_TIMEOUT_MS) {
89
+ if (await getJson(port, "/api/version")) return true;
90
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
91
+ }
92
+ return false;
93
+ }
94
+
95
+ function requireCommands() {
96
+ if (!hasCommand("ollama")) {
97
+ error("`ollama` not found on PATH. Install it: https://ollama.com/download");
98
+ process.exit(1);
99
+ }
100
+ if (!hasCommand("claude")) {
101
+ error("`claude` (Claude Code) not found. Install it: npm install -g @anthropic-ai/claude-code");
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ // Start the private ollama server and return a `stop` that kills it. The stop is wired to
107
+ // this process's own exit and signals so the server never outlives the launcher.
108
+ function startPrivateOllama(host) {
109
+ const serve = spawn("ollama", ["serve"], {
110
+ stdio: "ignore",
111
+ env: buildOllamaServeEnv(process.env, host, OLLAMA_CONTEXT_LENGTH),
112
+ });
113
+ let stopped = false;
114
+ const stop = () => {
115
+ if (stopped) return;
116
+ stopped = true;
117
+ try {
118
+ serve.kill("SIGTERM");
119
+ } catch {
120
+ // already gone
121
+ }
122
+ };
123
+ process.on("exit", stop);
124
+ for (const signal of ["SIGINT", "SIGTERM"]) {
125
+ process.on(signal, () => {
126
+ stop();
127
+ process.exit(signal === "SIGINT" ? 130 : 143);
128
+ });
129
+ }
130
+ serve.on("error", (e) => {
131
+ error(`failed to start ollama serve: ${e.message}`);
132
+ process.exit(1);
133
+ });
134
+ return stop;
135
+ }
136
+
137
+ // Hand off to interactive Claude Code; exit with its code and stop our server behind it.
138
+ function runClaude(model, baseUrl, claudeArgs, stopServe) {
139
+ log(`launching Claude Code on ${model} (minimal prompt: --bare --disable-slash-commands)`);
140
+ const claude = spawn("claude", buildClaudeArgs(claudeArgs), {
141
+ stdio: "inherit",
142
+ env: buildClaudeEnv(process.env, model, baseUrl),
143
+ });
144
+ claude.on("error", (e) => {
145
+ error(`failed to launch claude: ${e.message}`);
146
+ stopServe();
147
+ process.exit(1);
148
+ });
149
+ claude.on("exit", (code) => {
150
+ stopServe();
151
+ process.exit(code ?? 0);
152
+ });
153
+ }
154
+
155
+ async function main() {
156
+ const { help, model, claudeArgs } = parseClaudeOllamaArgs(process.argv.slice(2));
157
+ if (help || !model) {
158
+ printHelp();
159
+ process.exit(help ? 0 : 1);
160
+ }
161
+ requireCommands();
162
+ const port = await findFreePort();
163
+ const host = `127.0.0.1:${port}`;
164
+ log(`starting a private Ollama server on ${host} (context ${OLLAMA_CONTEXT_LENGTH})…`);
165
+ const stopServe = startPrivateOllama(host);
166
+ if (!(await waitUntilReady(port))) {
167
+ error("the Ollama server did not become ready in time");
168
+ stopServe();
169
+ process.exit(1);
170
+ }
171
+ const tags = await getJson(port, "/api/tags");
172
+ if (tags && !modelIsInstalled(tags, model)) {
173
+ error(`model '${model}' is not installed. Pull it first: ollama pull ${model}`);
174
+ stopServe();
175
+ process.exit(1);
176
+ }
177
+ runClaude(model, `http://${host}`, claudeArgs, stopServe);
178
+ }
179
+
180
+ main().catch((e) => {
181
+ error(e?.message || String(e));
182
+ process.exit(1);
183
+ });
@@ -0,0 +1,11 @@
1
+ export type PortChoice = { port: number; explicit: boolean } | { error: string };
2
+ export type CwdChoice = { path: string; mustExist: boolean } | { error: string };
3
+ export declare function parsePortArg(args: string[], defaultPort: number): PortChoice;
4
+ export declare function chooseCwd(args: string[], env: Record<string, string | undefined>): CwdChoice;
5
+ export declare function portInUseMessage(port: number, explicit: boolean): string;
6
+ export declare function portInUseAction(explicit: boolean, isTTY: boolean | undefined): "ask" | "stop";
7
+ export declare function secondInstancePrompt(port: number): string;
8
+ export declare function saysYes(answer: unknown): boolean;
9
+ export declare const SECOND_INSTANCE_NOTE: string;
10
+ export declare const MIN_NODE_LABEL: string;
11
+ export declare function nodeMeetsMinimum(version: string): boolean;
@@ -0,0 +1,135 @@
1
+ // What the launcher decides before it starts anything, kept out of the executable so each
2
+ // decision can be checked without a process to exit or a terminal to type into.
3
+ //
4
+ // `--cwd` picks the workspace claude runs in and whose sessions the sidebar lists, so it is
5
+ // a data-scope boundary: getting it wrong points the whole app at someone else's project.
6
+ // `--port` decides whether a clash is a hard error or a silent retry. Both used to be
7
+ // decided inside the executable, where they exit the process on a bad value and so could not
8
+ // be checked at all (#611 A3).
9
+ //
10
+ // These return a decision; the caller prints and exits. Nothing here reads argv, the
11
+ // environment or the filesystem.
12
+
13
+ /**
14
+ * Which port the launcher should ask for.
15
+ * `--port` must be a plain integer in range: a value that merely starts with digits ("80x")
16
+ * or carries a sign or padding ("+80", "080") is a typo, and silently launching on 80 is
17
+ * worse than saying so.
18
+ */
19
+ export function parsePortArg(args, defaultPort) {
20
+ const at = args.indexOf("--port");
21
+ if (at === -1) return { port: defaultPort, explicit: false };
22
+ const raw = args[at + 1];
23
+ const parsed = Number.parseInt(raw ?? "", 10);
24
+ if (!Number.isInteger(parsed) || String(parsed) !== raw || parsed < 1 || parsed > 65535) {
25
+ return { error: `Invalid --port value: "${raw ?? ""}" (expected integer 1..65535)` };
26
+ }
27
+ return { port: parsed, explicit: true };
28
+ }
29
+
30
+ /**
31
+ * Which directory to run in, before it is made absolute.
32
+ * Precedence: `--cwd` (relative allowed) > CLAUDE_CWD > the directory the launcher was run
33
+ * from. `mustExist` is set only for `--cwd`: a typo there should stop the launch, while a
34
+ * CLAUDE_CWD naming a directory that isn't there yet is the managed-workspace case the
35
+ * server creates on boot.
36
+ */
37
+ export function chooseCwd(args, env) {
38
+ const at = args.indexOf("--cwd");
39
+ if (at === -1) return { path: env.CLAUDE_CWD ?? ".", mustExist: false };
40
+ const value = args[at + 1];
41
+ // A missing value swallows the next flag ("--cwd --port 3000" would run in a directory
42
+ // called "--port"), so anything flag-shaped is treated as absent.
43
+ if (value === undefined || value.startsWith("-")) return { error: "--cwd requires a directory path" };
44
+ return { path: value, mustExist: true };
45
+ }
46
+
47
+ /**
48
+ * What to say when the port is taken.
49
+ *
50
+ * Running a second server is not a supported setup: both share ~/.mulmoterminal and the
51
+ * workspace, but each keeps its own PTYs, pub/sub and in-memory caches, so the two disagree
52
+ * about state neither can see the other change. Starting one silently on another port —
53
+ * which is what a plain second `npx mulmoterminal` used to do — is how someone ends up in
54
+ * that setup without knowing (#611).
55
+ *
56
+ * So the message has to answer the two things the user actually wants: where the running one
57
+ * is, and how to insist if a second is really wanted.
58
+ */
59
+ export function portInUseMessage(port, explicit) {
60
+ const lines = [`Port ${port} is already in use.`];
61
+ lines.push(` If that is MulmoTerminal, it is already running at http://localhost:${port}`);
62
+ lines.push(explicit ? " Pick a different --port, or stop the other process." : " To start a second one anyway: --port <number>");
63
+ return lines.join("\n");
64
+ }
65
+
66
+ /**
67
+ * What to do when the wanted port is taken: "ask" whether to start a second instance,
68
+ * or "stop".
69
+ *
70
+ * Two conditions rule the question out. An explicit --port already named the port that
71
+ * was wanted, so offering a different one answers a question nobody asked; and with no
72
+ * terminal to type into (a script, a service, CI) a prompt has nobody to answer it and
73
+ * would hang the start instead of failing it.
74
+ */
75
+ export function portInUseAction(explicit, isTTY) {
76
+ return explicit || !isTTY ? "stop" : "ask";
77
+ }
78
+
79
+ /**
80
+ * The question asked when the default port is taken and there is somebody to answer.
81
+ *
82
+ * Two servers is not a supported setup (#611), but it is a legitimate thing to want on
83
+ * purpose — so the answer is a question rather than a refusal, and the default is no.
84
+ */
85
+ export function secondInstancePrompt(port) {
86
+ return [`Port ${port} is already in use — MulmoTerminal may already be running at http://localhost:${port}`, "Start a second instance anyway? [y/N] "].join(
87
+ "\n",
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Whether an answer to that question is a yes.
93
+ *
94
+ * Only an explicit yes counts. The prompt says [y/N], so Enter — and anything unrecognised —
95
+ * means no: starting a second server by misreading a stray keystroke is the outcome worth
96
+ * avoiding, and saying no costs one retyped command.
97
+ */
98
+ export function saysYes(answer) {
99
+ return /^y(es)?$/i.test(String(answer ?? "").trim());
100
+ }
101
+
102
+ /**
103
+ * What someone who said yes should know before the second one comes up.
104
+ *
105
+ * One line, and only the part that is still true: config and the hidden-session list are
106
+ * safe across instances, but a session's tool history is cached per process and its live
107
+ * updates never cross — so the instance that did not start a session shows a frozen history
108
+ * for it (#611).
109
+ */
110
+ export const SECOND_INSTANCE_NOTE = [
111
+ " Note: both share ~/.mulmoterminal. A session's tool history does not live-update",
112
+ " in the instance that did not start it.",
113
+ ].join("\n");
114
+
115
+ const MIN_NODE_MAJOR = 22;
116
+ const MIN_NODE_MINOR = 9;
117
+
118
+ /**
119
+ * The lowest Node the `init` check reports as good, for the "needs ≥ x.y" line.
120
+ */
121
+ export const MIN_NODE_LABEL = `${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}`;
122
+
123
+ /**
124
+ * Whether the running Node is new enough for the `init` pre-flight tick.
125
+ *
126
+ * `process.versions.node` is "major.minor.patch", with a "-prerelease" tag on the patch for
127
+ * nightlies ("22.9.0-nightly…"). Only major.minor gate, and Number.parseInt stops at the
128
+ * first non-digit, so that tag never reaches the comparison. A string that is not a version
129
+ * parses to NaN, and every comparison against NaN is false, so an unreadable version reads as
130
+ * "below minimum" — the safe direction for a display-only check.
131
+ */
132
+ export function nodeMeetsMinimum(version) {
133
+ const [major, minor] = version.split(".").map((part) => Number.parseInt(part, 10));
134
+ return major > MIN_NODE_MAJOR || (major === MIN_NODE_MAJOR && minor >= MIN_NODE_MINOR);
135
+ }