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.
- package/README.md +56 -13
- package/bin/claude-ollama.js +183 -0
- package/bin/cli-args.d.ts +11 -0
- package/bin/cli-args.js +135 -0
- package/bin/mulmoterminal.js +71 -88
- package/bin/ollama-launch.d.ts +13 -0
- package/bin/ollama-launch.js +60 -0
- package/bin/update-check.d.ts +21 -0
- package/bin/update-check.js +164 -2
- package/common/median.ts +13 -0
- package/common/modelIds.ts +28 -0
- package/common/modelPresets.ts +313 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-6nNByj6v-BmTAK_lf.js → abnfDiagram-VRR7QNED-6nNByj6v-BCJQa4DV.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-DgJ3pu6_.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bgnyaj_i-Rm4oPVV8.js → architectureDiagram-ZJ3FMSHR-Bgnyaj_i-D_Nt7RIE.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-DQ35o5E4-CpSusKj_.js → blockDiagram-677ZJIJ3-DQ35o5E4-BILj_7MF.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-ClWZeiWo-CDPWcIZ7.js → c4Diagram-LMCZKHZV-ClWZeiWo-CUaWnhEC.js} +1 -1
- package/dist/assets/channel-Di5rtkx0-Bi7KHChX.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-CCt9wtYd-BNsLemkR.js → chunk-32BRIVSS-CCt9wtYd-CH_vt2Gl.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-FbBbR4uI-hDAV5e1Z.js → chunk-52WLFC77-FbBbR4uI-DyZrk4KQ.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-C87hlS9c-DfzZj0PL.js → chunk-C7G6YPKG-C87hlS9c-CFx1Arp1.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BqGqMXLN-uoc2Ztzj.js → chunk-EX3LRPZG-BqGqMXLN-CiSc3YI0.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-BkbSAAuW-B397K7iE.js → chunk-FWX5IMBZ-BkbSAAuW-B6Frv5LB.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Cxu0eDlh-BgHFcMoQ.js → chunk-HOUHSVGY-Cxu0eDlh-PsDgJqDO.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-hiraF_Xj-BJT-y8zi.js → chunk-ICXQ74PX-hiraF_Xj-DYPMB6hF.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CqxshQHA-rfYFgPHl.js → chunk-MOJQB5TN-CqxshQHA-gM_q16Ft.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-CjCr7ceX-DuZ8UI47.js → chunk-OGEWGWER-CjCr7ceX-9qc1rmAh.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-Dx6M-vz1-C20tVRvs.js → chunk-PUDLZKDR-Dx6M-vz1-CbyfAYMR.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-CZd-9lTB-BFkl1myD.js → chunk-Q4XR5HBZ-CZd-9lTB-CHAmO99-.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-rI0xlC_O-C2QrFL3o.js → chunk-V7JOEXUC-rI0xlC_O-CpbdluPv.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-DrcykVNK-BSJoRdEM.js → chunk-VAUOI2AC-DrcykVNK-DFpod4Mi.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-O6iF8Yvf-CL4V93u8.js → chunk-VR4S4FIN-O6iF8Yvf-BDT221yg.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-B83L_z6I-ybwRHaqV.js → chunk-WYO6CB5R-B83L_z6I-Djv0Pedo.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BpFv9JSP-DQGEhX3k.js → chunk-ZGVPDNZ5-BpFv9JSP-X8DR2YEI.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-B5BBPEcr.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-B5BBPEcr.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-CNt3aNde.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-x0-0sQ15-B3DfOJaL.js → cynefinDiagram-TSTJHNR4-x0-0sQ15-CDpp4wje.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-CQdfl-bx-Bf4AA431.js → dagre-VKFMJZFB-CQdfl-bx-MejOSRzt.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BOSB6VUb-BO7S37GV.js → diagram-FQU43EPY-BOSB6VUb-B55SUPg7.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-DLXrcXsN-Cw4lBqWi.js → diagram-G47NLZAW-DLXrcXsN-BtHyFrEv.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-BMQp1rkF-B4OObXRC.js → diagram-NH7WQ7WH-BMQp1rkF-DAo6PIHz.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-D1wQ0vUj-CbSsGWhY.js → diagram-OA4YK3LP-D1wQ0vUj-BOP-rGWN.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-RR0DpF8R-CkVYiUn6.js → diagram-WEI45ONY-RR0DpF8R-DMkTz0bs.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-M123uVJ8-BIFkvee9.js → ebnfDiagram-CCIWWBDH-M123uVJ8-DYQvurZD.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-BWx_-PXG-DhFy59tl.js → erDiagram-Q63AITRT-BWx_-PXG-NRsUZc6C.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-DckF6nty.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-BRAAyPH5.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-BOoJ1eTw-Cy64oRyD.js → ganttDiagram-NO4QXBWP-BOoJ1eTw-im9hAj69.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-C3PN2LIe.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-B2CJhk_G-Do02StAh.js → gitGraphDiagram-IHSO6WYX-B2CJhk_G-BZV7bMgx.js} +1 -1
- package/dist/assets/index-Cue6y3NP.css +1 -0
- package/dist/assets/index-JiqffS7s.js +617 -0
- package/dist/assets/info-DKCQHKI2-Cbw3mbiK-BAo5Fb7g.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-Mp1X3pBP-CTODYevC.js → infoDiagram-FWYZ7A6U-Mp1X3pBP-n9QeRL84.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-BNG7tkJu-DWG68Fod.js → ishikawaDiagram-FXEZZL3T-BNG7tkJu-BaCscIGZ.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-Dbp_hY9X-C21LZ7jk.js → journeyDiagram-5HDEW3XC-Dbp_hY9X-Dt8UlK_2.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DSTc5u3q-ahPtq3pf.js → kanban-definition-HUTT4EX6-DSTc5u3q-DUVHu497.js} +1 -1
- package/dist/assets/{lib-Df1jlz1Q.js → lib-1OP8J0D5.js} +1 -1
- package/dist/assets/{line-B1wBwzrY-DAl65sYb.js → line-B1wBwzrY-BcbT6-ul.js} +1 -1
- package/dist/assets/{marp-D5H57UyA.js → marp-GfniLWYo.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DC7NPJ_M-QXgmJCuN.js → mermaid-parser.core-DC7NPJ_M-B3PfPUiA.js} +2 -2
- package/dist/assets/{mermaid.core-DZM3Ha-E-B1G-85N2.js → mermaid.core-DZM3Ha-E-BWGnyKvd.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-DYtgcMsY-CBnOdPk8.js → mindmap-definition-LN4V7U3C-DYtgcMsY-Bi04e3Dh.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-lwb58iYx-B_vqCkVp.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-C43eIpKM-hEGXt4rK.js → pegDiagram-2B236MQR-C43eIpKM-D5nABd5d.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-CAPwLWC2.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-BkTqgJyR-D84c_7gF.js → pieDiagram-ENE6RG2P-BkTqgJyR-BYsX0pmp.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-Bm1Zjm45-m6ws2UZn.js → quadrantDiagram-ABIIQ3AL-Bm1Zjm45-CpkA29IU.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-7CKcb_l--YAwbZSSO.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-gCySKdnW-DWNa-BLR.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--8kHN0m0s.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-BeQb-Rfm.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-fqlxInkH.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6-BkfbdeAs-BN3y7GqL.js → railroadDiagram-RFXS5EU6-BkfbdeAs-DljCkwrn.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-CrGTTjYg-B0zoTBgF.js → requirementDiagram-TGXJPOKE-CrGTTjYg-C9JLg12J.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-rWXPf03Z-XgG2MUCe.js → sankeyDiagram-HTMAVEWB-rWXPf03Z-DDL7fuaw.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-nkJYWO2m-BxK5UjRj.js → sequenceDiagram-DBY2YBRQ-nkJYWO2m-Pyu_gxuA.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-T4-clK8b-Y_DtKuon.js → stateDiagram-2N3HPSRC-T4-clK8b-CYg9qZQS.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-DPkjtkgF.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-HmQNEntu-D5dAJGJF.js → swimlanes-5IMT3BWC-HmQNEntu-DEKxjcNL.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-B2wFtrEU.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CNc9jSTP-CRCAKMND.js → timeline-definition-FHXFAJF6-CNc9jSTP-DpFI7Gdx.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-CxCShMBw.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-BFi0W9rh.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-Dr3pTJ_0-DIFYkt_z.js → vennDiagram-L72KCM5P-Dr3pTJ_0-Dmra9rkk.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-2Yl3qxOe.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-DSFc7ZZa-BL_kqA2P.js → wardleyDiagram-EHGQE667-DSFc7ZZa-CbUid2zT.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-BP0Nn4Pp-q75mTWUr.js → xychartDiagram-FW5EYKEG-BP0Nn4Pp-BJc0ZRNl.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/server/agents/claude-args.ts +5 -0
- package/server/agents/codex-activity.ts +70 -0
- package/server/agents/codex-resume.ts +26 -0
- package/server/backends/audioAdmission.ts +62 -0
- package/server/backends/byte-range.ts +40 -0
- package/server/backends/collectionNotifierAdapter.ts +57 -0
- package/server/backends/collectionWatchers.ts +9 -49
- package/server/backends/collections.ts +22 -18
- package/server/backends/docPath.ts +53 -0
- package/server/backends/feed-summary.ts +27 -0
- package/server/backends/feeds.ts +2 -9
- package/server/backends/fileChange.ts +5 -8
- package/server/backends/files.ts +10 -52
- package/server/backends/image-gen.ts +2 -18
- package/server/backends/imageResult.ts +28 -0
- package/server/backends/markdown.ts +2 -30
- package/server/backends/mutateStatus.ts +28 -0
- package/server/backends/rawServingPlan.ts +56 -0
- package/server/backends/remoteHost/attachment-path.ts +31 -0
- package/server/backends/remoteHost/attachmentStore.ts +1 -24
- package/server/backends/remoteHost/handlers.ts +2 -9
- package/server/backends/remoteHost/index.ts +5 -12
- package/server/backends/remoteHost/onExpire.ts +2 -16
- package/server/backends/remoteHost/sessionActivity.ts +11 -1
- package/server/backends/remoteHost/stagedStorageIds.spec.ts +38 -0
- package/server/backends/remoteHost/stagedStorageIds.ts +16 -0
- package/server/backends/remoteView.ts +17 -3
- package/server/backends/whisper.ts +6 -53
- package/server/config/app-config.ts +29 -1
- package/server/config/config-body.ts +27 -0
- package/server/config/config-routes.ts +24 -20
- package/server/config/config-schema.ts +38 -0
- package/server/config/dir-config.ts +9 -0
- package/server/config/launch-options.ts +45 -0
- package/server/config/update-status.ts +38 -0
- package/server/git/branch-query.ts +29 -0
- package/server/git/git-parse.ts +34 -0
- package/server/git/pr-for-branch.ts +3 -11
- package/server/git/prPhase.ts +3 -10
- package/server/git/worktree-diff.ts +3 -4
- package/server/git/worktree-pr.ts +2 -10
- package/server/index.ts +102 -882
- package/server/infra/allowed-origin.ts +24 -0
- package/server/infra/plugins-registry.ts +20 -9
- package/server/infra/process-guards.ts +32 -0
- package/server/infra/server-exit.ts +30 -0
- package/server/infra/server-tool-load.ts +29 -0
- package/server/infra/tmux.ts +21 -1
- package/server/infra/tool-precedence.ts +43 -0
- package/server/mcp/broker.ts +18 -49
- package/server/mcp/tool-envelope.ts +38 -0
- package/server/mcp/tool-gate.ts +63 -0
- package/server/routes/app-routes.ts +283 -0
- package/server/routes/dir-routes.ts +2 -1
- package/server/routes/hook-routes.ts +165 -0
- package/server/routes/mcp-routes.ts +54 -0
- package/server/routes/plugin-narration.ts +18 -0
- package/server/routes/plugin-routes.ts +103 -0
- package/server/routes/routeParams.ts +19 -0
- package/server/routes/session-routes.ts +20 -25
- package/server/routes/terminal-ws-path.ts +25 -0
- package/server/routes/tool-routes.ts +8 -27
- package/server/routes/toolResultPlan.ts +39 -0
- package/server/routes/ws-routes.ts +61 -46
- package/server/session/activity-flag.ts +31 -0
- package/server/session/activity-hook.ts +18 -1
- package/server/session/activity-state.ts +17 -0
- package/server/session/activity-transition.ts +64 -0
- package/server/session/background-chat.ts +47 -0
- package/server/session/codex-activity-track.ts +69 -0
- package/server/session/codex-activity-watch.ts +51 -0
- package/server/session/dev-terminal-sessions.ts +54 -0
- package/server/session/draft-injection.ts +4 -4
- package/server/session/draft-plan.ts +21 -0
- package/server/session/handoff-text.ts +17 -1
- package/server/session/header-hook.ts +38 -0
- package/server/session/hiddenMarker.ts +15 -0
- package/server/session/hook-settings.ts +57 -0
- package/server/session/launch-choice.ts +71 -0
- package/server/session/lifecycle.ts +209 -0
- package/server/session/mcp-config.ts +36 -0
- package/server/session/partitionPending.ts +39 -0
- package/server/session/provider-env.ts +147 -0
- package/server/session/pty-spawn.ts +21 -6
- package/server/session/reap-policy.ts +36 -0
- package/server/session/registry.ts +60 -19
- package/server/session/resumable-sessions.ts +18 -0
- package/server/session/scheduled-sessions.ts +9 -0
- package/server/session/session-detail-view.ts +54 -0
- package/server/session/session-list.ts +8 -6
- package/server/session/session-reads.ts +28 -21
- package/server/session/session-settings.ts +45 -0
- package/server/session/session-title.ts +103 -0
- package/server/session/sessionListTitle.ts +22 -0
- package/server/session/spawn-claude.ts +55 -23
- package/server/session/spawn-codex.ts +19 -2
- package/server/session/spawn-deps.ts +7 -2
- package/server/session/spawners.ts +15 -0
- package/server/session/task-push.ts +54 -0
- package/server/session/taskPushRules.ts +33 -0
- package/server/session/tool-hook.ts +59 -0
- package/server/session/tool-store.ts +63 -15
- package/server/session/translation-submit.ts +27 -0
- package/server/session/work-phase-tracker.ts +56 -0
- package/server/skills/mulmoterminal-config/SKILL.md +58 -2
- package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-ChEc_7wP.js +0 -1
- package/dist/assets/channel-Di5rtkx0-CW5bKrWi.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-BJBeI7ut.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-BJBeI7ut.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-CHrZLRBv.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-Dows47_h.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-D8EOvOHn.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-D0iLvwj3.js +0 -1
- package/dist/assets/index-CkheN-4M.css +0 -1
- package/dist/assets/index-D7iecJBA.js +0 -607
- package/dist/assets/info-DKCQHKI2-Cbw3mbiK-BnqrlbY_.js +0 -1
- package/dist/assets/packet-7NZHBO7P-lwb58iYx-vktMe7EJ.js +0 -1
- package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-DQdd8O0g.js +0 -1
- package/dist/assets/radar-I7S5WNFK-7CKcb_l--C24Fc6mu.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-gCySKdnW-CZSGevii.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--CUsXkbxZ.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-CvkVfCff.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-DVG8HAZ0.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-Cehiuymv.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-CoSHy1jT.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-CWUjubva.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-BKgXJcXq.js +0 -1
- 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
|

|
|
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
|
|
30
|
-
phone**, and the **RemoteHost** companion lets you watch and
|
|
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
|
+

|
|
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
|
|
104
|
-
|
|
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
|
|
672
|
-
|
|
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,
|
|
702
|
-
collection views. You can also merge
|
|
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;
|
package/bin/cli-args.js
ADDED
|
@@ -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
|
+
}
|