mulmoterminal 1.5.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.
- package/README.md +30 -3
- 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-w5RLkQ22.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-BUaZ0MSo.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bgnyaj_i-Rm4oPVV8.js → architectureDiagram-ZJ3FMSHR-Bgnyaj_i-D69YcDkW.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-DQ35o5E4-CpSusKj_.js → blockDiagram-677ZJIJ3-DQ35o5E4-7uQXieKy.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-ClWZeiWo-CDPWcIZ7.js → c4Diagram-LMCZKHZV-ClWZeiWo-EXpL5q8c.js} +1 -1
- package/dist/assets/channel-Di5rtkx0-C_m9yiey.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-CCt9wtYd-BNsLemkR.js → chunk-32BRIVSS-CCt9wtYd-BBNy9Wsw.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-FbBbR4uI-hDAV5e1Z.js → chunk-52WLFC77-FbBbR4uI-nrizj9KQ.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-C87hlS9c-DfzZj0PL.js → chunk-C7G6YPKG-C87hlS9c-BeUrCw4a.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BqGqMXLN-uoc2Ztzj.js → chunk-EX3LRPZG-BqGqMXLN-B6S4Nm2k.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-BkbSAAuW-B397K7iE.js → chunk-FWX5IMBZ-BkbSAAuW-DyThgqZX.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Cxu0eDlh-BgHFcMoQ.js → chunk-HOUHSVGY-Cxu0eDlh-D5RjKEII.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-hiraF_Xj-BJT-y8zi.js → chunk-ICXQ74PX-hiraF_Xj-JuRpIiaL.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CqxshQHA-rfYFgPHl.js → chunk-MOJQB5TN-CqxshQHA-D3AD-OQe.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-CjCr7ceX-DuZ8UI47.js → chunk-OGEWGWER-CjCr7ceX-CqjmtDlt.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-Dx6M-vz1-C20tVRvs.js → chunk-PUDLZKDR-Dx6M-vz1-DnwunR7_.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-CZd-9lTB-BFkl1myD.js → chunk-Q4XR5HBZ-CZd-9lTB-CTRA-UzE.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-rI0xlC_O-C2QrFL3o.js → chunk-V7JOEXUC-rI0xlC_O-yFVHoSz8.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-DrcykVNK-BSJoRdEM.js → chunk-VAUOI2AC-DrcykVNK-Bu5JrdaI.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-O6iF8Yvf-CL4V93u8.js → chunk-VR4S4FIN-O6iF8Yvf-bQ7KsnFU.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-B83L_z6I-ybwRHaqV.js → chunk-WYO6CB5R-B83L_z6I-Db0d_mXD.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BpFv9JSP-DQGEhX3k.js → chunk-ZGVPDNZ5-BpFv9JSP-D6iQstYB.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-DEFx7H3k.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-DEFx7H3k.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-BCw_wwOv.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-x0-0sQ15-B3DfOJaL.js → cynefinDiagram-TSTJHNR4-x0-0sQ15-DX3rOamM.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-CQdfl-bx-Bf4AA431.js → dagre-VKFMJZFB-CQdfl-bx-CMvy6K78.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BOSB6VUb-BO7S37GV.js → diagram-FQU43EPY-BOSB6VUb-7Cloywn1.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-DLXrcXsN-Cw4lBqWi.js → diagram-G47NLZAW-DLXrcXsN-BTVMn6-x.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-BMQp1rkF-B4OObXRC.js → diagram-NH7WQ7WH-BMQp1rkF-YaK2dWz0.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-D1wQ0vUj-CbSsGWhY.js → diagram-OA4YK3LP-D1wQ0vUj-D3A1N794.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-RR0DpF8R-CkVYiUn6.js → diagram-WEI45ONY-RR0DpF8R-mQyI-Ztc.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-M123uVJ8-BIFkvee9.js → ebnfDiagram-CCIWWBDH-M123uVJ8-6QHlDAzo.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-BWx_-PXG-DhFy59tl.js → erDiagram-Q63AITRT-BWx_-PXG-Dm9LOTEX.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-CAJF7-9m.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-CLQEzYDX.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-BOoJ1eTw-Cy64oRyD.js → ganttDiagram-NO4QXBWP-BOoJ1eTw-DF8P-WOj.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-CuWNblKk.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-B2CJhk_G-Do02StAh.js → gitGraphDiagram-IHSO6WYX-B2CJhk_G-C1VDCiQA.js} +1 -1
- package/dist/assets/index-BFfKUWpL.js +617 -0
- package/dist/assets/index-aQgV74tC.css +1 -0
- package/dist/assets/info-DKCQHKI2-Cbw3mbiK-rc7lkPBD.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-Mp1X3pBP-CTODYevC.js → infoDiagram-FWYZ7A6U-Mp1X3pBP-BPoQawfx.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-BNG7tkJu-DWG68Fod.js → ishikawaDiagram-FXEZZL3T-BNG7tkJu-lE_jc2TT.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-Dbp_hY9X-C21LZ7jk.js → journeyDiagram-5HDEW3XC-Dbp_hY9X-CO0R6cU-.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-DSTc5u3q-ahPtq3pf.js → kanban-definition-HUTT4EX6-DSTc5u3q-CxGAv9XA.js} +1 -1
- package/dist/assets/{lib-Df1jlz1Q.js → lib-DRGyh9Q-.js} +1 -1
- package/dist/assets/{line-B1wBwzrY-DAl65sYb.js → line-B1wBwzrY-CRPsJOzA.js} +1 -1
- package/dist/assets/{marp-D5H57UyA.js → marp--aqCYout.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DC7NPJ_M-QXgmJCuN.js → mermaid-parser.core-DC7NPJ_M-4_BR6vAg.js} +2 -2
- package/dist/assets/{mermaid.core-DZM3Ha-E-B1G-85N2.js → mermaid.core-DZM3Ha-E-QA2iKs10.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-DYtgcMsY-CBnOdPk8.js → mindmap-definition-LN4V7U3C-DYtgcMsY-dUsG7ytl.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-lwb58iYx-BRjtDgmF.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-C43eIpKM-hEGXt4rK.js → pegDiagram-2B236MQR-C43eIpKM-C0qYy0kY.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-DjuYUfZC.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-BkTqgJyR-D84c_7gF.js → pieDiagram-ENE6RG2P-BkTqgJyR-p9ruWXka.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-Bm1Zjm45-m6ws2UZn.js → quadrantDiagram-ABIIQ3AL-Bm1Zjm45-D6JMmK1K.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-7CKcb_l--BSS9MxXL.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-gCySKdnW-hpLOScUf.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--D67cs0T8.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-D64IlM4-.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-Da5GoZb9.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6-BkfbdeAs-BN3y7GqL.js → railroadDiagram-RFXS5EU6-BkfbdeAs-DKGrmtAb.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-CrGTTjYg-B0zoTBgF.js → requirementDiagram-TGXJPOKE-CrGTTjYg-_rK4hExQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-rWXPf03Z-XgG2MUCe.js → sankeyDiagram-HTMAVEWB-rWXPf03Z-CtaFLD0A.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-nkJYWO2m-BxK5UjRj.js → sequenceDiagram-DBY2YBRQ-nkJYWO2m-Cfwgbl-y.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-T4-clK8b-Y_DtKuon.js → stateDiagram-2N3HPSRC-T4-clK8b-DHGQRUOm.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-Ud1cpdGy.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-HmQNEntu-D5dAJGJF.js → swimlanes-5IMT3BWC-HmQNEntu-D869r1NM.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-DUGuyc0H.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CNc9jSTP-CRCAKMND.js → timeline-definition-FHXFAJF6-CNc9jSTP-BeazEwi2.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-u70as5aK.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-CkNsgl3O.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-Dr3pTJ_0-DIFYkt_z.js → vennDiagram-L72KCM5P-Dr3pTJ_0-CMOqalF2.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-DsDgGaTC.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-DSFc7ZZa-BL_kqA2P.js → wardleyDiagram-EHGQE667-DSFc7ZZa-CEPlNlLn.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-BP0Nn4Pp-q75mTWUr.js → xychartDiagram-FW5EYKEG-BP0Nn4Pp-Bvdb_Uk8.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/handlers.ts +2 -9
- package/server/backends/remoteHost/index.ts +5 -12
- package/server/backends/remoteHost/onExpire.ts +2 -16
- 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 +88 -883
- package/server/infra/allowed-origin.ts +24 -0
- package/server/infra/plugins-registry.ts +20 -9
- 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 +280 -0
- package/server/routes/dir-routes.ts +2 -1
- package/server/routes/hook-routes.ts +157 -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 +15 -22
- 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 +47 -45
- 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/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 +200 -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-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/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
|
@@ -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
|
|
@@ -668,8 +692,10 @@ Each grid cell's header shows two badges for its session, refreshed when a turn
|
|
|
668
692
|
|
|
669
693
|
- **Context badge** — e.g. `Opus · ctx 35%`: the model family plus how full its context
|
|
670
694
|
window is (the *last* turn's input + cache tokens ÷ the model's window — **1M** for
|
|
671
|
-
current-gen Opus / Sonnet / Fable, **200k** otherwise
|
|
672
|
-
|
|
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.
|
|
673
699
|
- **Token badge** — `⇡<in> ⇣<out>`: cumulative input (fresh + cache-read + cache-creation)
|
|
674
700
|
and output tokens for the session, k/M-formatted, with a full breakdown in the tooltip.
|
|
675
701
|
|
|
@@ -938,8 +964,9 @@ same-origin-guarded.
|
|
|
938
964
|
|
|
939
965
|
| Endpoint | Purpose |
|
|
940
966
|
| -------- | ------- |
|
|
941
|
-
| `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`). |
|
|
942
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. |
|
|
943
970
|
| `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
|
|
944
971
|
| `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
|
|
945
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;
|
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
|
+
}
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -13,14 +13,24 @@ import { createServer } from "node:net";
|
|
|
13
13
|
import { dirname, join, resolve } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
16
|
+
import { computeUpdateNotice, isUpdateCheckDisabled } from "./update-check.js";
|
|
17
|
+
import {
|
|
18
|
+
chooseCwd,
|
|
19
|
+
parsePortArg,
|
|
20
|
+
portInUseAction,
|
|
21
|
+
portInUseMessage,
|
|
22
|
+
saysYes,
|
|
23
|
+
secondInstancePrompt,
|
|
24
|
+
SECOND_INSTANCE_NOTE,
|
|
25
|
+
nodeMeetsMinimum,
|
|
26
|
+
MIN_NODE_LABEL,
|
|
27
|
+
} from "./cli-args.js";
|
|
17
28
|
|
|
18
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
30
|
const PKG_DIR = join(__dirname, "..");
|
|
20
31
|
const SERVER_ENTRY = join(PKG_DIR, "server", "index.ts");
|
|
21
32
|
const DEFAULT_PORT = 34567;
|
|
22
33
|
const READY_TIMEOUT_MS = 15_000;
|
|
23
|
-
const MAX_BIND_RETRIES = 5;
|
|
24
34
|
// Server exit code meaning "port taken at bind time" — keep in sync with
|
|
25
35
|
// server/index.ts (PORT_IN_USE_EXIT_CODE).
|
|
26
36
|
const PORT_IN_USE_EXIT_CODE = 75;
|
|
@@ -32,19 +42,18 @@ const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
|
32
42
|
const log = (msg) => console.log(`\x1b[36m[mulmoterminal]\x1b[0m ${msg}`);
|
|
33
43
|
const error = (msg) => console.error(`\x1b[31m[mulmoterminal]\x1b[0m ${msg}`);
|
|
34
44
|
|
|
35
|
-
// Non-blocking notice
|
|
36
|
-
// auto-updates. Opt out via MULMOTERMINAL_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
45
|
+
// Non-blocking console notice that a newer version exists — neither `npm i -g` nor a git
|
|
46
|
+
// checkout auto-updates. Opt out via MULMOTERMINAL_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER. The
|
|
47
|
+
// same check runs in the server for the web-header badge (the launcher isn't involved under
|
|
48
|
+
// `yarn dev`), sharing computeUpdateNotice so the two never drift.
|
|
49
|
+
async function checkForUpdate() {
|
|
50
|
+
if (isUpdateCheckDisabled(process.env)) return;
|
|
51
|
+
try {
|
|
52
|
+
const notice = await computeUpdateNotice(PKG_DIR, VERSION);
|
|
53
|
+
if (notice) log(`\x1b[33m${notice}\x1b[0m`);
|
|
54
|
+
} catch {
|
|
55
|
+
// best-effort; never disrupt startup
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
// Detect a CLI on the user's PATH by asking for its version. Intentionally resolves from
|
|
@@ -52,7 +61,6 @@ function checkForUpdate() {
|
|
|
52
61
|
// `init` checks.
|
|
53
62
|
function hasCommand(cmd, versionArg = "--version") {
|
|
54
63
|
try {
|
|
55
|
-
// eslint-disable-next-line sonarjs/no-os-command-from-path
|
|
56
64
|
execSync(`${cmd} ${versionArg}`, { stdio: "pipe" });
|
|
57
65
|
return true;
|
|
58
66
|
} catch {
|
|
@@ -69,7 +77,7 @@ function promptYesNo(question) {
|
|
|
69
77
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
78
|
rl.question(question, (answer) => {
|
|
71
79
|
rl.close();
|
|
72
|
-
res(
|
|
80
|
+
res(saysYes(answer));
|
|
73
81
|
});
|
|
74
82
|
});
|
|
75
83
|
}
|
|
@@ -80,9 +88,8 @@ function promptYesNo(question) {
|
|
|
80
88
|
async function runInit(initArgs) {
|
|
81
89
|
log("Setting up MulmoTerminal…\n");
|
|
82
90
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
console.log(nodeOk ? ` ✓ Node ${process.versions.node}` : ` ✗ Node ${process.versions.node} — MulmoTerminal needs ≥ 22.9`);
|
|
91
|
+
const nodeOk = nodeMeetsMinimum(process.versions.node);
|
|
92
|
+
console.log(nodeOk ? ` ✓ Node ${process.versions.node}` : ` ✗ Node ${process.versions.node} — MulmoTerminal needs ≥ ${MIN_NODE_LABEL}`);
|
|
86
93
|
|
|
87
94
|
const hasClaude = claudeInstalled();
|
|
88
95
|
if (hasClaude) {
|
|
@@ -120,7 +127,6 @@ async function runInit(initArgs) {
|
|
|
120
127
|
// must never block waiting on stdin.
|
|
121
128
|
if (hasClaude && process.stdin.isTTY && (await promptYesNo("\nConfigure interactively now with the /mulmoterminal-config skill? [y/N] "))) {
|
|
122
129
|
log("Launching Claude — use /mulmoterminal-config (or just ask it to configure MulmoTerminal).");
|
|
123
|
-
// eslint-disable-next-line sonarjs/no-os-command-from-path
|
|
124
130
|
spawn("claude", ["Use the mulmoterminal-config skill to configure MulmoTerminal."], { stdio: "inherit" });
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
@@ -164,23 +170,6 @@ function isPortFree(port) {
|
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
// Ask the OS for a free port (listen on 0) and return the one it assigned, or
|
|
168
|
-
// null. An effectively-random fallback when the preferred port is taken; the
|
|
169
|
-
// bind-retry in main() closes the small probe-to-bind race so concurrent starts
|
|
170
|
-
// don't clash.
|
|
171
|
-
function findEphemeralPort() {
|
|
172
|
-
return new Promise((resolve) => {
|
|
173
|
-
const probe = createServer();
|
|
174
|
-
probe.once("error", () => resolve(null));
|
|
175
|
-
probe.once("listening", () => {
|
|
176
|
-
const addr = probe.address();
|
|
177
|
-
const assigned = addr && typeof addr === "object" ? addr.port : null;
|
|
178
|
-
probe.close(() => resolve(assigned));
|
|
179
|
-
});
|
|
180
|
-
probe.listen(0);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
173
|
// Poll the server until it answers, then call onReady; give up after the timeout
|
|
185
174
|
// so the launcher never hangs on a crash loop. Returns a cancel function — a
|
|
186
175
|
// raced/abandoned attempt stops polling so it can't fire a stale banner.
|
|
@@ -220,60 +209,65 @@ function printReadyBanner(url) {
|
|
|
220
209
|
console.log(`${bar}\n`);
|
|
221
210
|
}
|
|
222
211
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!Number.isInteger(parsed) || String(parsed) !== raw || parsed < 1 || parsed > 65535) {
|
|
229
|
-
error(`Invalid --port value: "${raw ?? ""}" (expected integer 1..65535)`);
|
|
212
|
+
// The two flag decisions live in cli-args.js so they can be checked without a process to
|
|
213
|
+
// exit; this turns a decision into the message and the exit.
|
|
214
|
+
function decideOrExit(choice) {
|
|
215
|
+
if ("error" in choice) {
|
|
216
|
+
error(choice.error);
|
|
230
217
|
process.exit(1);
|
|
231
218
|
}
|
|
232
|
-
return
|
|
219
|
+
return choice;
|
|
233
220
|
}
|
|
234
221
|
|
|
235
222
|
// Resolve the workspace directory claude runs in (and whose sessions the sidebar
|
|
236
|
-
// lists).
|
|
237
|
-
//
|
|
238
|
-
// isn't an existing directory is a hard error (catches typos before launch).
|
|
223
|
+
// lists), always absolute. An explicit --cwd that isn't an existing directory is a hard
|
|
224
|
+
// error (catches typos before launch); an inherited one is the workspace the server creates.
|
|
239
225
|
function resolveCwd(args) {
|
|
240
|
-
const
|
|
241
|
-
let flagValue;
|
|
242
|
-
if (idx !== -1) {
|
|
243
|
-
flagValue = args[idx + 1];
|
|
244
|
-
if (flagValue === undefined || flagValue.startsWith("-")) {
|
|
245
|
-
error("--cwd requires a directory path");
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
const chosen = flagValue ?? process.env.CLAUDE_CWD ?? ".";
|
|
226
|
+
const { path: chosen, mustExist } = decideOrExit(chooseCwd(args, process.env));
|
|
250
227
|
const abs = resolve(process.cwd(), chosen);
|
|
251
|
-
if (
|
|
228
|
+
if (mustExist && (!existsSync(abs) || !statSync(abs).isDirectory())) {
|
|
252
229
|
error(`--cwd is not a directory: ${abs}`);
|
|
253
230
|
process.exit(1);
|
|
254
231
|
}
|
|
255
232
|
return abs;
|
|
256
233
|
}
|
|
257
234
|
|
|
235
|
+
// Ask the OS for a free port (listen on 0) and return the one it assigned, or null. Only
|
|
236
|
+
// reached once someone has said yes to a second instance.
|
|
237
|
+
function findEphemeralPort() {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
const probe = createServer();
|
|
240
|
+
probe.once("error", () => resolve(null));
|
|
241
|
+
probe.once("listening", () => {
|
|
242
|
+
const { port } = probe.address();
|
|
243
|
+
probe.close(() => resolve(port));
|
|
244
|
+
});
|
|
245
|
+
probe.listen(0);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
258
249
|
async function choosePort(requested, explicit) {
|
|
259
250
|
if (await isPortFree(requested)) return requested;
|
|
260
|
-
|
|
261
|
-
|
|
251
|
+
// No SILENT fallback: starting a second server on another port without saying so is how
|
|
252
|
+
// someone ends up with two sharing one home directory without knowing (#611).
|
|
253
|
+
if (portInUseAction(explicit, process.stdin.isTTY) === "stop") {
|
|
254
|
+
error(portInUseMessage(requested, explicit));
|
|
262
255
|
process.exit(1);
|
|
263
256
|
}
|
|
257
|
+
if (!(await promptYesNo(secondInstancePrompt(requested)))) process.exit(1);
|
|
264
258
|
const fallback = await findEphemeralPort();
|
|
265
259
|
if (fallback === null) {
|
|
266
|
-
error(
|
|
260
|
+
error("No free port could be found for a second instance.");
|
|
267
261
|
process.exit(1);
|
|
268
262
|
}
|
|
269
|
-
log(
|
|
263
|
+
log(SECOND_INSTANCE_NOTE);
|
|
270
264
|
return fallback;
|
|
271
265
|
}
|
|
272
266
|
|
|
273
267
|
// Spawn the server on `port` and report the child via `onChild` (so signal
|
|
274
268
|
// handlers target the live process). Resolves only when the server exits because
|
|
275
269
|
// the port was taken at bind time before it became ready — the caller then
|
|
276
|
-
//
|
|
270
|
+
// reports that and stops. In every other case (clean shutdown, fatal error,
|
|
277
271
|
// or the server simply running) the process exits with the server's code.
|
|
278
272
|
function runServer(port, noOpen, cwd, onChild) {
|
|
279
273
|
return new Promise((resolveExit) => {
|
|
@@ -326,7 +320,9 @@ Commands:
|
|
|
326
320
|
|
|
327
321
|
Options:
|
|
328
322
|
--cwd <dir> Working directory claude runs in (default: current directory; relative paths allowed)
|
|
329
|
-
--port <number> Server port (default: ${DEFAULT_PORT}
|
|
323
|
+
--port <number> Server port (default: ${DEFAULT_PORT}). If it is in use, you are asked
|
|
324
|
+
whether to start a second instance on a free port; with --port, or
|
|
325
|
+
with no terminal to ask, startup stops instead.
|
|
330
326
|
--no-open Don't open the browser automatically
|
|
331
327
|
--version Show version
|
|
332
328
|
--help Show this help
|
|
@@ -369,7 +365,7 @@ async function main() {
|
|
|
369
365
|
process.exit(1);
|
|
370
366
|
}
|
|
371
367
|
|
|
372
|
-
const { requestedPort, portExplicit } = parsePortArg(args);
|
|
368
|
+
const { port: requestedPort, explicit: portExplicit } = decideOrExit(parsePortArg(args, DEFAULT_PORT));
|
|
373
369
|
const noOpen = args.includes("--no-open");
|
|
374
370
|
const cwd = resolveCwd(args);
|
|
375
371
|
log(`Workspace: ${cwd}`);
|
|
@@ -383,27 +379,14 @@ async function main() {
|
|
|
383
379
|
process.on("SIGINT", shutdown);
|
|
384
380
|
process.on("SIGTERM", shutdown);
|
|
385
381
|
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (portExplicit) {
|
|
395
|
-
error(`Port ${port} is already in use. Stop the other process or pick a different --port.`);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
const next = await findEphemeralPort();
|
|
399
|
-
if (next === null) {
|
|
400
|
-
error("No free port available to retry on.");
|
|
401
|
-
process.exit(1);
|
|
402
|
-
}
|
|
403
|
-
log(`Port ${port} was taken at bind time → retrying on ${next}.`);
|
|
404
|
-
port = next;
|
|
405
|
-
}
|
|
406
|
-
error(`Could not bind a free port after ${MAX_BIND_RETRIES + 1} attempts.`);
|
|
382
|
+
// The probe above can still lose to something binding the port in the same instant, in
|
|
383
|
+
// which case the server exits 75 and runServer returns. Same answer as the probe: say who
|
|
384
|
+
// has it rather than moving to a port nobody asked for.
|
|
385
|
+
const port = await choosePort(requestedPort, portExplicit);
|
|
386
|
+
await runServer(port, noOpen, cwd, (c) => {
|
|
387
|
+
child = c;
|
|
388
|
+
});
|
|
389
|
+
error(portInUseMessage(port, portExplicit));
|
|
407
390
|
process.exit(1);
|
|
408
391
|
}
|
|
409
392
|
|