mulmoterminal 1.4.0 → 1.6.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 (260) hide show
  1. package/README.md +47 -5
  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 +23 -0
  9. package/bin/update-check.js +164 -2
  10. package/common/dirChrome.ts +20 -0
  11. package/common/median.ts +13 -0
  12. package/common/modelIds.ts +28 -0
  13. package/common/modelPresets.ts +313 -0
  14. package/common/themeIds.ts +6 -0
  15. package/dist/assets/{abnfDiagram-VRR7QNED-6nNByj6v-Ca_1xL9E.js → abnfDiagram-VRR7QNED-6nNByj6v-w5RLkQ22.js} +1 -1
  16. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-BUaZ0MSo.js +1 -0
  17. package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bgnyaj_i-DeqNwmGo.js → architectureDiagram-ZJ3FMSHR-Bgnyaj_i-D69YcDkW.js} +1 -1
  18. package/dist/assets/{blockDiagram-677ZJIJ3-DQ35o5E4-BMHxAWeR.js → blockDiagram-677ZJIJ3-DQ35o5E4-7uQXieKy.js} +1 -1
  19. package/dist/assets/{c4Diagram-LMCZKHZV-ClWZeiWo-SZJrYJpa.js → c4Diagram-LMCZKHZV-ClWZeiWo-EXpL5q8c.js} +1 -1
  20. package/dist/assets/channel-Di5rtkx0-C_m9yiey.js +1 -0
  21. package/dist/assets/{chunk-32BRIVSS-CCt9wtYd-Pb9wd8dr.js → chunk-32BRIVSS-CCt9wtYd-BBNy9Wsw.js} +1 -1
  22. package/dist/assets/{chunk-52WLFC77-FbBbR4uI-BRpr0cAc.js → chunk-52WLFC77-FbBbR4uI-nrizj9KQ.js} +1 -1
  23. package/dist/assets/{chunk-C7G6YPKG-C87hlS9c-Bp5UQi3A.js → chunk-C7G6YPKG-C87hlS9c-BeUrCw4a.js} +1 -1
  24. package/dist/assets/{chunk-EX3LRPZG-BqGqMXLN-CNBYB1hW.js → chunk-EX3LRPZG-BqGqMXLN-B6S4Nm2k.js} +1 -1
  25. package/dist/assets/{chunk-FWX5IMBZ-BkbSAAuW-C8pwvYr9.js → chunk-FWX5IMBZ-BkbSAAuW-DyThgqZX.js} +2 -2
  26. package/dist/assets/{chunk-HOUHSVGY-Cxu0eDlh-qXR3o3_Q.js → chunk-HOUHSVGY-Cxu0eDlh-D5RjKEII.js} +1 -1
  27. package/dist/assets/{chunk-ICXQ74PX-hiraF_Xj-D6RChm8S.js → chunk-ICXQ74PX-hiraF_Xj-JuRpIiaL.js} +1 -1
  28. package/dist/assets/{chunk-MOJQB5TN-CqxshQHA-DzpdT-_-.js → chunk-MOJQB5TN-CqxshQHA-D3AD-OQe.js} +1 -1
  29. package/dist/assets/{chunk-OGEWGWER-CjCr7ceX-CHwPg6wN.js → chunk-OGEWGWER-CjCr7ceX-CqjmtDlt.js} +1 -1
  30. package/dist/assets/{chunk-PUDLZKDR-Dx6M-vz1-1LjvELJl.js → chunk-PUDLZKDR-Dx6M-vz1-DnwunR7_.js} +1 -1
  31. package/dist/assets/{chunk-Q4XR5HBZ-CZd-9lTB-H2jDDOca.js → chunk-Q4XR5HBZ-CZd-9lTB-CTRA-UzE.js} +1 -1
  32. package/dist/assets/{chunk-V7JOEXUC-rI0xlC_O-DsYxdI8K.js → chunk-V7JOEXUC-rI0xlC_O-yFVHoSz8.js} +1 -1
  33. package/dist/assets/{chunk-VAUOI2AC-DrcykVNK-BG02s6nm.js → chunk-VAUOI2AC-DrcykVNK-Bu5JrdaI.js} +1 -1
  34. package/dist/assets/{chunk-VR4S4FIN-O6iF8Yvf-BkxcJW6l.js → chunk-VR4S4FIN-O6iF8Yvf-bQ7KsnFU.js} +1 -1
  35. package/dist/assets/{chunk-WYO6CB5R-B83L_z6I-BLR3Im7t.js → chunk-WYO6CB5R-B83L_z6I-Db0d_mXD.js} +1 -1
  36. package/dist/assets/{chunk-ZGVPDNZ5-BpFv9JSP-DreP7ALc.js → chunk-ZGVPDNZ5-BpFv9JSP-D6iQstYB.js} +1 -1
  37. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-DEFx7H3k.js +1 -0
  38. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-DEFx7H3k.js +1 -0
  39. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-BCw_wwOv.js +1 -0
  40. package/dist/assets/{cynefinDiagram-TSTJHNR4-x0-0sQ15-CG2HDCNn.js → cynefinDiagram-TSTJHNR4-x0-0sQ15-DX3rOamM.js} +1 -1
  41. package/dist/assets/{dagre-VKFMJZFB-CQdfl-bx-C_u-VAtp.js → dagre-VKFMJZFB-CQdfl-bx-CMvy6K78.js} +1 -1
  42. package/dist/assets/{diagram-FQU43EPY-BOSB6VUb-Co4MItV_.js → diagram-FQU43EPY-BOSB6VUb-7Cloywn1.js} +1 -1
  43. package/dist/assets/{diagram-G47NLZAW-DLXrcXsN-CpQs0brV.js → diagram-G47NLZAW-DLXrcXsN-BTVMn6-x.js} +1 -1
  44. package/dist/assets/{diagram-NH7WQ7WH-BMQp1rkF-d3plBnN8.js → diagram-NH7WQ7WH-BMQp1rkF-YaK2dWz0.js} +1 -1
  45. package/dist/assets/{diagram-OA4YK3LP-D1wQ0vUj-CrBfGx8L.js → diagram-OA4YK3LP-D1wQ0vUj-D3A1N794.js} +1 -1
  46. package/dist/assets/{diagram-WEI45ONY-RR0DpF8R-CS-M3Ra0.js → diagram-WEI45ONY-RR0DpF8R-mQyI-Ztc.js} +1 -1
  47. package/dist/assets/{ebnfDiagram-CCIWWBDH-M123uVJ8-OUbCegYV.js → ebnfDiagram-CCIWWBDH-M123uVJ8-6QHlDAzo.js} +1 -1
  48. package/dist/assets/{erDiagram-Q63AITRT-BWx_-PXG-CEyrlpra.js → erDiagram-Q63AITRT-BWx_-PXG-Dm9LOTEX.js} +1 -1
  49. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-CAJF7-9m.js +1 -0
  50. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-CLQEzYDX.js +1 -0
  51. package/dist/assets/{ganttDiagram-NO4QXBWP-BOoJ1eTw-BjxqJeU6.js → ganttDiagram-NO4QXBWP-BOoJ1eTw-DF8P-WOj.js} +1 -1
  52. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-CuWNblKk.js +1 -0
  53. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B2CJhk_G-CsW7_CEO.js → gitGraphDiagram-IHSO6WYX-B2CJhk_G-C1VDCiQA.js} +1 -1
  54. package/dist/assets/index-BFfKUWpL.js +617 -0
  55. package/dist/assets/index-aQgV74tC.css +1 -0
  56. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-rc7lkPBD.js +1 -0
  57. package/dist/assets/{infoDiagram-FWYZ7A6U-Mp1X3pBP-B1W9eCMy.js → infoDiagram-FWYZ7A6U-Mp1X3pBP-BPoQawfx.js} +1 -1
  58. package/dist/assets/{ishikawaDiagram-FXEZZL3T-BNG7tkJu-Q8Yh-Oes.js → ishikawaDiagram-FXEZZL3T-BNG7tkJu-lE_jc2TT.js} +1 -1
  59. package/dist/assets/{journeyDiagram-5HDEW3XC-Dbp_hY9X-maN4pjfe.js → journeyDiagram-5HDEW3XC-Dbp_hY9X-CO0R6cU-.js} +1 -1
  60. package/dist/assets/{kanban-definition-HUTT4EX6-DSTc5u3q-DSlStVjd.js → kanban-definition-HUTT4EX6-DSTc5u3q-CxGAv9XA.js} +1 -1
  61. package/dist/assets/{lib-BgqM-YBP.js → lib-DRGyh9Q-.js} +1 -1
  62. package/dist/assets/{line-B1wBwzrY-DiyABYDp.js → line-B1wBwzrY-CRPsJOzA.js} +1 -1
  63. package/dist/assets/{marp-BjuRL9gF.js → marp--aqCYout.js} +1 -1
  64. package/dist/assets/{mermaid-parser.core-DC7NPJ_M-C2yu1mqW.js → mermaid-parser.core-DC7NPJ_M-4_BR6vAg.js} +2 -2
  65. package/dist/assets/{mermaid.core-DZM3Ha-E-BmNFXjNa.js → mermaid.core-DZM3Ha-E-QA2iKs10.js} +3 -3
  66. package/dist/assets/{mindmap-definition-LN4V7U3C-DYtgcMsY-Hz0ORILX.js → mindmap-definition-LN4V7U3C-DYtgcMsY-dUsG7ytl.js} +1 -1
  67. package/dist/assets/packet-7NZHBO7P-lwb58iYx-BRjtDgmF.js +1 -0
  68. package/dist/assets/{pegDiagram-2B236MQR-C43eIpKM-Pqw-eDjm.js → pegDiagram-2B236MQR-C43eIpKM-C0qYy0kY.js} +1 -1
  69. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-DjuYUfZC.js +1 -0
  70. package/dist/assets/{pieDiagram-ENE6RG2P-BkTqgJyR-DDYDtXLw.js → pieDiagram-ENE6RG2P-BkTqgJyR-p9ruWXka.js} +1 -1
  71. package/dist/assets/{quadrantDiagram-ABIIQ3AL-Bm1Zjm45-BWk3t4WO.js → quadrantDiagram-ABIIQ3AL-Bm1Zjm45-D6JMmK1K.js} +1 -1
  72. package/dist/assets/radar-I7S5WNFK-7CKcb_l--BSS9MxXL.js +1 -0
  73. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-hpLOScUf.js +1 -0
  74. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--D67cs0T8.js +1 -0
  75. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-D64IlM4-.js +1 -0
  76. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-Da5GoZb9.js +1 -0
  77. package/dist/assets/{railroadDiagram-RFXS5EU6-BkfbdeAs-B6bHQo3P.js → railroadDiagram-RFXS5EU6-BkfbdeAs-DKGrmtAb.js} +1 -1
  78. package/dist/assets/{requirementDiagram-TGXJPOKE-CrGTTjYg-CNJlZb8l.js → requirementDiagram-TGXJPOKE-CrGTTjYg-_rK4hExQ.js} +1 -1
  79. package/dist/assets/{sankeyDiagram-HTMAVEWB-rWXPf03Z-D0W5vOfI.js → sankeyDiagram-HTMAVEWB-rWXPf03Z-CtaFLD0A.js} +1 -1
  80. package/dist/assets/{sequenceDiagram-DBY2YBRQ-nkJYWO2m-CMxA7B3A.js → sequenceDiagram-DBY2YBRQ-nkJYWO2m-Cfwgbl-y.js} +1 -1
  81. package/dist/assets/{stateDiagram-2N3HPSRC-T4-clK8b-B9saurhU.js → stateDiagram-2N3HPSRC-T4-clK8b-DHGQRUOm.js} +1 -1
  82. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-Ud1cpdGy.js +1 -0
  83. package/dist/assets/{swimlanes-5IMT3BWC-HmQNEntu-BODdb8qy.js → swimlanes-5IMT3BWC-HmQNEntu-D869r1NM.js} +1 -1
  84. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-DUGuyc0H.js +8 -0
  85. package/dist/assets/{timeline-definition-FHXFAJF6-CNc9jSTP-CJf7rTMs.js → timeline-definition-FHXFAJF6-CNc9jSTP-BeazEwi2.js} +1 -1
  86. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-u70as5aK.js +1 -0
  87. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-CkNsgl3O.js +1 -0
  88. package/dist/assets/{vennDiagram-L72KCM5P-Dr3pTJ_0-DyKxIhWC.js → vennDiagram-L72KCM5P-Dr3pTJ_0-CMOqalF2.js} +1 -1
  89. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-DsDgGaTC.js +1 -0
  90. package/dist/assets/{wardleyDiagram-EHGQE667-DSFc7ZZa-CelZHvd2.js → wardleyDiagram-EHGQE667-DSFc7ZZa-CEPlNlLn.js} +1 -1
  91. package/dist/assets/{xychartDiagram-FW5EYKEG-BP0Nn4Pp-BoBAD26H.js → xychartDiagram-FW5EYKEG-BP0Nn4Pp-Bvdb_Uk8.js} +1 -1
  92. package/dist/index.html +2 -2
  93. package/package.json +13 -13
  94. package/server/agents/claude-args.ts +5 -0
  95. package/server/agents/codex-activity.ts +70 -0
  96. package/server/agents/codex-resume.ts +26 -0
  97. package/server/agents/codex-sessions.ts +14 -8
  98. package/server/backends/audioAdmission.ts +62 -0
  99. package/server/backends/byte-range.ts +40 -0
  100. package/server/backends/collectionNotifierAdapter.ts +57 -0
  101. package/server/backends/collectionWatchers.ts +9 -49
  102. package/server/backends/collections.ts +303 -387
  103. package/server/backends/docPath.ts +53 -0
  104. package/server/backends/feed-summary.ts +27 -0
  105. package/server/backends/feeds.ts +3 -22
  106. package/server/backends/fileChange.ts +5 -8
  107. package/server/backends/files.ts +13 -64
  108. package/server/backends/html.ts +3 -12
  109. package/server/backends/image-gen.ts +2 -18
  110. package/server/backends/imageResult.ts +28 -0
  111. package/server/backends/markdown.ts +2 -30
  112. package/server/backends/mutateStatus.ts +28 -0
  113. package/server/backends/rawServingPlan.ts +56 -0
  114. package/server/backends/remoteHost/getFeed.spec.ts +9 -1
  115. package/server/backends/remoteHost/handlers.spec.ts +9 -1
  116. package/server/backends/remoteHost/handlers.ts +43 -23
  117. package/server/backends/remoteHost/index.ts +10 -9
  118. package/server/backends/remoteHost/onExpire.ts +2 -16
  119. package/server/backends/remoteHost/sessionActivity.ts +93 -0
  120. package/server/backends/remoteHost/skills.spec.ts +14 -3
  121. package/server/backends/remoteHost/stagedStorageIds.spec.ts +38 -0
  122. package/server/backends/remoteHost/stagedStorageIds.ts +16 -0
  123. package/server/backends/remoteHost/terminalInput.spec.ts +264 -0
  124. package/server/backends/remoteHost/terminalInput.ts +120 -0
  125. package/server/backends/remoteHost/terminalScreen.ts +42 -5
  126. package/server/backends/remoteView.ts +38 -14
  127. package/server/backends/shortcuts.ts +20 -1
  128. package/server/backends/statFileOr404.ts +20 -0
  129. package/server/backends/whisper.ts +6 -53
  130. package/server/config/app-config.ts +48 -14
  131. package/server/config/config-body.ts +27 -0
  132. package/server/config/config-routes.ts +26 -34
  133. package/server/config/config-schema.ts +39 -1
  134. package/server/config/dir-config.ts +16 -23
  135. package/server/config/env.ts +21 -0
  136. package/server/config/header-resolve.ts +7 -0
  137. package/server/config/launch-options.ts +45 -0
  138. package/server/config/update-status.ts +38 -0
  139. package/server/config/workspace.ts +23 -0
  140. package/server/errors.ts +6 -0
  141. package/server/files/atomic-write.ts +50 -0
  142. package/server/files/dirRequest.ts +30 -0
  143. package/server/files/open-dir.ts +3 -13
  144. package/server/git/branch-query.ts +29 -0
  145. package/server/git/dirty-count.ts +8 -0
  146. package/server/git/gh.ts +2 -17
  147. package/server/git/git-parse.ts +34 -0
  148. package/server/git/git-status.ts +1 -5
  149. package/server/git/gitRemote.ts +3 -16
  150. package/server/git/pr-for-branch.ts +8 -19
  151. package/server/git/prPhase.ts +8 -18
  152. package/server/git/spawn-collect.ts +23 -0
  153. package/server/git/ttl-cache.ts +24 -0
  154. package/server/git/worktree-diff.ts +4 -9
  155. package/server/git/worktree-pr.ts +7 -29
  156. package/server/git/worktrees.ts +14 -5
  157. package/server/index.ts +251 -2668
  158. package/server/infra/allowed-origin.ts +24 -0
  159. package/server/infra/plugins-registry.ts +20 -9
  160. package/server/infra/pty-env.ts +73 -0
  161. package/server/infra/sandbox.ts +117 -12
  162. package/server/infra/server-exit.ts +30 -0
  163. package/server/infra/server-tool-load.ts +29 -0
  164. package/server/infra/spawnCapture.ts +6 -0
  165. package/server/infra/tmux.ts +122 -14
  166. package/server/infra/tool-precedence.ts +43 -0
  167. package/server/infra/web-push.ts +5 -1
  168. package/server/mcp/broker.ts +18 -49
  169. package/server/mcp/tool-envelope.ts +38 -0
  170. package/server/mcp/tool-gate.ts +63 -0
  171. package/server/routes/app-routes.ts +280 -0
  172. package/server/routes/dir-routes.ts +105 -0
  173. package/server/routes/hook-routes.ts +157 -0
  174. package/server/routes/mcp-routes.ts +54 -0
  175. package/server/routes/plugin-narration.ts +18 -0
  176. package/server/routes/plugin-routes.ts +103 -0
  177. package/server/routes/repo-routes.ts +27 -0
  178. package/server/routes/routeParams.ts +19 -0
  179. package/server/routes/session-routes.ts +191 -0
  180. package/server/routes/terminal-ws-path.ts +25 -0
  181. package/server/routes/tool-routes.ts +64 -0
  182. package/server/routes/toolResultPlan.ts +39 -0
  183. package/server/routes/ws-routes.ts +374 -0
  184. package/server/session/activity-flag.ts +31 -0
  185. package/server/session/activity-hook.ts +102 -8
  186. package/server/session/activity-state.ts +17 -0
  187. package/server/session/activity-transition.ts +64 -0
  188. package/server/session/background-chat.ts +47 -0
  189. package/server/session/codex-activity-track.ts +69 -0
  190. package/server/session/codex-activity-watch.ts +51 -0
  191. package/server/session/cost.ts +1 -8
  192. package/server/session/dev-terminal-sessions.ts +54 -0
  193. package/server/session/draft-injection.ts +113 -0
  194. package/server/session/handoff-text.ts +65 -0
  195. package/server/session/header-hook.ts +38 -0
  196. package/server/session/headlessScreen.ts +18 -5
  197. package/server/session/hiddenMarker.ts +15 -0
  198. package/server/session/hook-settings.ts +57 -0
  199. package/server/session/last-turn.ts +84 -0
  200. package/server/session/launch-choice.ts +71 -0
  201. package/server/session/lifecycle.ts +200 -0
  202. package/server/session/mcp-config.ts +36 -0
  203. package/server/session/partitionPending.ts +39 -0
  204. package/server/session/project-dir.ts +37 -0
  205. package/server/session/provider-env.ts +147 -0
  206. package/server/session/pty-connection.ts +127 -0
  207. package/server/session/pty-spawn.ts +78 -0
  208. package/server/session/pty-text.ts +34 -0
  209. package/server/session/reap-policy.ts +63 -0
  210. package/server/session/registry.ts +204 -0
  211. package/server/session/resumable-sessions.ts +18 -0
  212. package/server/session/scheduled-sessions.ts +196 -0
  213. package/server/session/screen-rows.ts +123 -0
  214. package/server/session/session-detail-view.ts +54 -0
  215. package/server/session/session-list.ts +42 -0
  216. package/server/session/session-reads.ts +257 -0
  217. package/server/session/session-resolve.ts +24 -0
  218. package/server/session/session-settings.ts +45 -0
  219. package/server/session/session-title.ts +103 -0
  220. package/server/session/sessionListTitle.ts +22 -0
  221. package/server/session/shell-command.ts +28 -0
  222. package/server/session/spawn-claude.ts +143 -0
  223. package/server/session/spawn-codex.ts +95 -0
  224. package/server/session/spawn-deps.ts +24 -0
  225. package/server/session/spawn-shell.ts +69 -0
  226. package/server/session/spawners.ts +15 -0
  227. package/server/session/task-push.ts +54 -0
  228. package/server/session/taskPushRules.ts +33 -0
  229. package/server/session/tool-hook.ts +59 -0
  230. package/server/session/tool-store.ts +217 -0
  231. package/server/session/transcript.ts +5 -2
  232. package/server/session/translation-prompt.ts +28 -0
  233. package/server/session/translation-submit.ts +27 -0
  234. package/server/session/translation-worker.ts +122 -0
  235. package/server/session/types.ts +88 -0
  236. package/server/session/ws-frames.ts +51 -0
  237. package/server/skills/mulmoterminal-config/SKILL.md +58 -2
  238. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-ClOiEDNl.js +0 -1
  239. package/dist/assets/channel-Di5rtkx0-GApNIfgs.js +0 -1
  240. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-Xtn_iE77.js +0 -1
  241. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-Xtn_iE77.js +0 -1
  242. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-Dn_RgKdZ.js +0 -1
  243. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-fhNS4V9y.js +0 -1
  244. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-eY7_48Lq.js +0 -1
  245. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-9yyiQB6a.js +0 -1
  246. package/dist/assets/index-9sOccSQD.css +0 -1
  247. package/dist/assets/index-D4zpPW-X.js +0 -607
  248. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-BaoU6qkE.js +0 -1
  249. package/dist/assets/packet-7NZHBO7P-lwb58iYx-C643WJOR.js +0 -1
  250. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-C6kv_lLQ.js +0 -1
  251. package/dist/assets/radar-I7S5WNFK-7CKcb_l--D2gGTcyW.js +0 -1
  252. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-BrODqkjM.js +0 -1
  253. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--CiadAS_w.js +0 -1
  254. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-B1kY_BCV.js +0 -1
  255. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-fJ5cY9QP.js +0 -1
  256. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-C4fOci9j.js +0 -1
  257. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-BQN1H2ff.js +0 -8
  258. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-BUWbtLMe.js +0 -1
  259. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-D-ymqmfg.js +0 -1
  260. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-DgDcbSyj.js +0 -1
package/README.md CHANGED
@@ -12,9 +12,9 @@ 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
- ![MulmoTerminal's grid view — several coding agents running side by side](https://raw.githubusercontent.com/receptron/mulmoterminal/main/docs/guide/images/grid-2x2.png)
15
+ ![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
16
 
17
- *The grid is a **cockpit for parallel agents** — each cell is one agent (Claude or Codex). A cell's **border color signals its state** — working (blue), needs-you (amber — a permission or answer), idle — and an attention chime pulls you back to a stuck cell off-screen, so you can supervise many and only step in where you're called. (See the [Cost & token usage](#cost--token-usage) shot below for a single cell's live header.)*
17
+ *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.*
18
18
 
19
19
  ## Why you'll want it
20
20
 
@@ -94,6 +94,19 @@ listener and needs a browser **on the host**. Either way it needs a Desktop OAut
94
94
  as `~/.secrets/client_secret_*.json`; the refresh token lands in `~/.config/mulmo/google-token.json`
95
95
  and is **shared with MulmoClaude**, so one link per machine covers both apps.
96
96
 
97
+ **Local models (optional).** The package also ships `claude-ollama` — a one-command launcher that
98
+ runs Claude Code **fully locally against an [Ollama](https://ollama.com) model** (no cloud, no API
99
+ key). It starts a large-context Ollama server and launches `claude` with a minimal system prompt so
100
+ small models aren't drowned:
101
+
102
+ ```bash
103
+ ollama pull qwen3:4b
104
+ npx -p mulmoterminal claude-ollama qwen3:4b # or, if installed globally: claude-ollama qwen3:4b
105
+ ```
106
+
107
+ See [Local models with claude-ollama](https://receptron.github.io/mulmoterminal/guide/en/claude-ollama.html)
108
+ for the details and model notes.
109
+
97
110
  > **Already linked before the calendar-list / colour features?** They need a read scope your existing
98
111
  > link doesn't have, so `listCalendars` (and, in practice, `colors`) fail with an insufficient-scope
99
112
  > 403 until you re-authorize: **Settings → Google account → Unlink**, then sign in again (or re-run
@@ -225,6 +238,17 @@ today — **Claude Code** (the default) and **Codex**.
225
238
  cell's launch form and the Collections browser carry a **Claude / Codex** toggle (your
226
239
  choice is remembered).
227
240
 
241
+ **Other models.**
242
+ Claude Code can run against any **Anthropic-compatible** backend (OpenRouter, Moonshot, a
243
+ LiteLLM gateway). Backends are listed in `~/.mulmoterminal/config.json` under `providers`,
244
+ and their **keys are read from the server's environment** — never from a file the app
245
+ serves. A directory sets its default in `.mulmoterminal.json` (`provider` / `model`), and
246
+ each grid cell's launch form has a **MODEL** select that overrides it for one session,
247
+ listing ~27 curated models with the measured pass rate of a real tool-using task beside
248
+ each. A provider whose token can't be resolved **refuses to start** rather than falling
249
+ 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:
250
+ [Using another model via OpenRouter](https://receptron.github.io/mulmoterminal/guide/en/providers.html).
251
+
228
252
  **Skills for Codex.** Codex has no `/<slug>` slash commands, so on session setup
229
253
  MulmoTerminal **mirrors the workspace's `.claude/skills` into `~/.codex/skills`** (each
230
254
  mirrored directory carries a `.mt-mirror` marker so a re-sync overwrites what MulmoTerminal
@@ -483,6 +507,7 @@ yarn dev:client # Vite dev server only
483
507
 
484
508
  yarn build # type-check (vue-tsc) + vite build -> dist/
485
509
  yarn typecheck:server # type-check the server (tsconfig.server.json)
510
+ yarn typecheck:test # type-check the specs (tsconfig.test*.json)
486
511
  yarn server # run backend; serves dist/ + the APIs on :34567
487
512
  yarn test # vitest run
488
513
  ```
@@ -491,6 +516,12 @@ The backend is TypeScript run directly via `tsx` (no build step); `server/` is
491
516
  type-checked separately through `tsconfig.server.json` (`strict`), kept out of
492
517
  the main `build` so the two type-check independently.
493
518
 
519
+ Specs sit outside both of those projects, and vitest strips types rather than
520
+ checking them — so `yarn typecheck:test` is what keeps them honest. It mirrors
521
+ the same split: `tsconfig.test.json` (client specs, DOM + `.vue`) and
522
+ `tsconfig.test-server.json` (server specs, node). CI runs it alongside the
523
+ other two.
524
+
494
525
  In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
495
526
  `:34567`. In production, run `yarn build` then `yarn server` and open
496
527
  `http://localhost:34567`.
@@ -661,8 +692,10 @@ Each grid cell's header shows two badges for its session, refreshed when a turn
661
692
 
662
693
  - **Context badge** — e.g. `Opus · ctx 35%`: the model family plus how full its context
663
694
  window is (the *last* turn's input + cache tokens ÷ the model's window — **1M** for
664
- current-gen Opus / Sonnet / Fable, **200k** otherwise; an unknown model shows the label
665
- with no %).
695
+ current-gen Opus / Sonnet / Fable, **200k** otherwise). A session running on a
696
+ [provider model](#agents-claude--codex) shows that model's name and its published window
697
+ (`Kimi K2.7 Code · ctx 12%`); a model in neither list keeps the label and hides the %,
698
+ since the window is never guessed.
666
699
  - **Token badge** — `⇡<in> ⇣<out>`: cumulative input (fresh + cache-read + cache-creation)
667
700
  and output tokens for the session, k/M-formatted, with a full breakdown in the tooltip.
668
701
 
@@ -724,6 +757,13 @@ Favorited collections get their own toolbar buttons.
724
757
 
725
758
  - **Timeline** (🕘) — a read-only per-session activity timeline (tools run, newest first),
726
759
  from `GET /api/transcript/timeline`.
760
+ - **Bring another cell's turn here** (💬) — pick another terminal in the grid and its
761
+ **last completed turn** is pasted into *this* cell's input box, so you can have Claude
762
+ and Codex look at each other's work (or pull in a session running in a different repo).
763
+ The excerpt comes from the agent's own log, not the screen buffer, so it carries no
764
+ ANSI debris and nothing lost to scrollback. It is **pasted, never sent** — you read
765
+ what arrived and press Enter, in the cell you were already in. A turn still running
766
+ isn't available yet (Codex writes its rollout only once the turn ends).
727
767
  - **Tools pane** — the available GUI tools plus a live tool-call history for the active
728
768
  session.
729
769
  - **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
@@ -889,6 +929,7 @@ same-origin-guarded.
889
929
  | `GET /api/codex/sessions?cwd=` | Codex sessions for the project (from `~/.codex` rollouts), newest first. |
890
930
  | `GET /api/cost?cwd=&session=` | Estimated $ cost — session / today / month. |
891
931
  | `GET /api/transcript/timeline?session=&cwd=` | Per-session activity timeline (tools run). |
932
+ | `GET /api/transcript/last-turn?session=&cwd=&agent=` | A session's last completed exchange (`prompt`, `reply`) plus the `text` to paste into another terminal. `agent=codex` reads the codex rollout instead of the Claude transcript. |
892
933
 
893
934
  **Git & worktrees**
894
935
 
@@ -923,8 +964,9 @@ same-origin-guarded.
923
964
 
924
965
  | Endpoint | Purpose |
925
966
  | -------- | ------- |
926
- | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`). |
967
+ | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`, `providers`). |
927
968
  | `GET /api/sound` · `/api/dir-sound?cwd=` · `/api/dir-config?cwd=` | Custom / per-directory attention sound + per-dir config. |
969
+ | `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. |
928
970
  | `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
929
971
  | `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
930
972
  | `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
+ }