mulmoterminal 2.5.3 → 2.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 +117 -21
- package/bin/cli-args.d.ts +5 -0
- package/bin/cli-args.js +20 -0
- package/bin/instances.d.ts +10 -0
- package/bin/instances.js +115 -0
- package/bin/mulmoterminal.js +27 -2
- package/common/dirConfigSource.ts +6 -0
- package/common/dropUpload.ts +16 -0
- package/common/finiteNumber.ts +9 -0
- package/common/quickCommands.ts +3 -1
- package/common/rateLimits.ts +2 -3
- package/common/remoteHostHealth.ts +6 -0
- package/common/sessionMemo.ts +44 -0
- package/common/toolGroups.ts +37 -5
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CkfTvNiT.js → abnfDiagram-VRR7QNED-RjiYivmv-i7KLnuI-.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-DRxyAOZm.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-DU5fnnYi.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-CL3Or8CZ.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-DJtyRfct.js → blockDiagram-677ZJIJ3-BPuAJQRW-DFu8zII3.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-CaAB4hIh.js → c4Diagram-LMCZKHZV-C0LAqQso-CByG3jFJ.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-CryXErAP.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-C2pc6s6p.js → chunk-32BRIVSS-BRrYpgtb-Dc3x0Hgh.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-BiDQ5LS-.js → chunk-52WLFC77-BJ-ss3Xr-D6efVaUm.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-B6NLudrF.js → chunk-C7G6YPKG-BZEucKEL-CUdIiVHU.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-Dydj9mRL.js → chunk-EX3LRPZG-DLS6FBN1-BH94eoNV.js} +1 -1
- package/dist/assets/chunk-FWX5IMBZ-DHLSFw1H-Dw8tAp6e.js +2 -0
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-460OsWXy.js → chunk-HOUHSVGY-Bhlt8hXJ-CEurvfOt.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-CSp9RB18.js → chunk-ICXQ74PX-DwgHBX_g-DKrDQRbg.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-CC-mdXDP.js → chunk-MOJQB5TN-CMZRaeqt-Ch-WBypm.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-nrJtDZ0I.js → chunk-OGEWGWER-8Qy4a8b5-BfJ1_Ye1.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-D7s7UkTC.js → chunk-PUDLZKDR-DcrWQRYh-NggLAE5k.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-Vjuup2uc.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DmtrERIM.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-V1zSMg_1.js → chunk-V7JOEXUC-DmGdheTX-C8lSwSR3.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-C1PM97Ei.js → chunk-VAUOI2AC-CS9QJ4yz-DNlwzQL1.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-zf8Ln3gQ.js → chunk-VR4S4FIN-C6a91eNY-DX2Te6ga.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CgOwOaF2.js → chunk-WYO6CB5R-BlzOfotS-BAOXGFjf.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-C17Ongqc.js → chunk-ZGVPDNZ5-BYwxNFTK-CS40JTYJ.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-Bh9HKUjT.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-Bh9HKUjT.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CNhjzHRd.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--rvKNlp57.js → cynefinDiagram-TSTJHNR4-DHA9iPo--DUt8F5SJ.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-DAPicxb0.js → dagre-VKFMJZFB--oJKqXBZ-BhcFRdM7.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Cwk0uscZ.js → diagram-FQU43EPY-CcJCB9bG-CBj7RNID.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-CLLr3WWX.js → diagram-G47NLZAW-C_o-WGG1-DCAfQJQ2.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--YbeWvGrQ.js → diagram-NH7WQ7WH-CXJCYvY--133061sd.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-CCzLGMoX.js → diagram-OA4YK3LP-BMzeJ87A-DPt25PXB.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-C_vctp-i.js → diagram-WEI45ONY-D_93NKqo-Bb2DJ3v3.js} +1 -1
- package/dist/assets/dist-4eLWDv4V.js +11 -0
- package/dist/assets/dist-BmZVepIx.js +2 -0
- package/dist/assets/dist-CDpjkjbB.js +9 -0
- package/dist/assets/dist-CXRReMvS.js +1 -0
- package/dist/assets/dist-Ci7pnEU6.js +11 -0
- package/dist/assets/dist-Do1AxSN7.js +1 -0
- package/dist/assets/dist-DyY2Odgu.js +1 -0
- package/dist/assets/dist-FzB0wwv6.js +1 -0
- package/dist/assets/dist-Lnp6Clqz.js +1 -0
- package/dist/assets/dist-berHSMWa.js +1 -0
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-C9RE0jib.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-CqvyVeoa.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-DNe_dsG7.js → erDiagram-Q63AITRT-CiABnA0s-jrNyKW5P.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BzmuvEpa.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-D_olhK5u.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-D0fBB7vz.js → ganttDiagram-NO4QXBWP-DQZvdFo1-Dz1MAsy4.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-6E2-f_ab.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-_9JEvhUj.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-CV3qv2yf.js} +1 -1
- package/dist/assets/index-CbJGZlpM.js +616 -0
- package/dist/assets/index-Zds0SlQi.css +1 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-DxJGF13X.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-sf0LZ0TR.js → infoDiagram-FWYZ7A6U-7UnoB5AP-BZUR4_mA.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-Dh3uJwRU.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-BLGvRDOX.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BaIw2Adh.js → journeyDiagram-5HDEW3XC-c5xIah9o-D9zqo5k7.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-BLIQlGsE.js → kanban-definition-HUTT4EX6-Cnt6loYD-BwfjKxOx.js} +1 -1
- package/dist/assets/lib-BWq7d2p0.js +9 -0
- package/dist/assets/{line-D7ziSjKi-BnFPtJcd.js → line-D7ziSjKi-ZOy-21z8.js} +1 -1
- package/dist/assets/{marp-JKciXjDr.js → marp-IdFBx6Kk.js} +37 -37
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-CscC8Qqr.js → mermaid-parser.core-DxEa8E3F-C_pDG6kL.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-g362i9RA.js → mermaid.core-V0OYwIz3--tUcNILl.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-CEmgqxM2.js → mindmap-definition-LN4V7U3C-CexN3O6L-B08uU8xg.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-C1R5Sk6K.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-xR2ciIPB.js → pegDiagram-2B236MQR-BiM4G0cW-CAmtHOOs.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-fnUCv-1K.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-Do2rjuXg.js → pieDiagram-ENE6RG2P-B_fBBS-2-1Te1k2OW.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CYjBsLx8.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-Bwmyhd3u.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DfxsmsH_.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-Cc9H2ZqM.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CedxYTf_.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DFUbcN03.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-7OkZDahF.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-DrRDvGFv.js → railroadDiagram-RFXS5EU6--lNIlG64-B7viF_TQ.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-AohyoXS1.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-C5VA5j5p.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-BeuhDKAh.js → sankeyDiagram-HTMAVEWB-DlutaXDv-DfD-musw.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-BMS6Xghs.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-DeYKXJK9.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-1ccT8EBF.js → stateDiagram-2N3HPSRC-jf_dXUEU-Bq6BSHXK.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-cQTUIwVP.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-B7FjNmUf.js → swimlanes-5IMT3BWC-CYjtALQH-BwKcwh8j.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-SzBmSlrT.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-Cv5APELs.js → timeline-definition-FHXFAJF6-DYJ4oUm8-D5NlLypk.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-mArgqXY_.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DcjdNdM5.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-DDVhQ5s2.js → vennDiagram-L72KCM5P-CdKAHoek-BAq00OzF.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-C9OSQ_B-.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BGrTblgg.js → wardleyDiagram-EHGQE667-BF6c4_CW-Dvh8PYFx.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-D9-hceiL.js → xychartDiagram-FW5EYKEG-CGiKngj7-C2sC6_Lu.js} +1 -1
- package/dist/index.html +3 -2
- package/package.json +15 -4
- package/server/agents/appended-prompt.ts +28 -0
- package/server/agents/claude-args.ts +11 -12
- package/server/agents/claude.ts +1 -0
- package/server/agents/codex-args.ts +30 -5
- package/server/agents/codex-rate-limits.spec.ts +20 -0
- package/server/agents/codex-rate-limits.ts +16 -6
- package/server/agents/codex-resume.ts +1 -1
- package/server/agents/codex-rollout.spec.ts +46 -0
- package/server/agents/codex-rollout.ts +13 -23
- package/server/agents/codex.ts +1 -0
- package/server/agents/probe-session.spec.ts +18 -0
- package/server/agents/probe-session.ts +10 -2
- package/server/agents/probe-transcript.spec.ts +282 -0
- package/server/agents/probe-transcript.ts +190 -0
- package/server/agents/rate-limit-persist.spec.ts +89 -0
- package/server/agents/rate-limit-persist.ts +24 -10
- package/server/agents/rate-limit-probe.spec.ts +52 -0
- package/server/agents/rate-limit-probe.ts +6 -2
- package/server/agents/rate-limit-store.spec.ts +15 -0
- package/server/agents/rate-limit-store.ts +8 -2
- package/server/agents/statusline.spec.ts +12 -3
- package/server/agents/statusline.ts +8 -7
- package/server/agents/types.ts +3 -0
- package/server/backends/collections.ts +15 -3
- package/server/backends/feed-summary.ts +1 -1
- package/server/backends/fileChange.ts +19 -6
- package/server/backends/fileOps.ts +5 -1
- package/server/backends/html.ts +67 -9
- package/server/backends/imageResult.ts +2 -2
- package/server/backends/markdown.ts +20 -9
- package/server/backends/mulmoscript.spec.ts +3 -3
- package/server/backends/openPath.ts +67 -0
- package/server/backends/remoteHost/googleCalendar.ts +9 -4
- package/server/backends/remoteHost/handlers.ts +2 -1
- package/server/backends/remoteHost/index.ts +18 -5
- package/server/backends/remoteHost/sessionActivity.ts +27 -3
- package/server/backends/remoteHost/terminalInput.ts +1 -1
- package/server/backends/remoteHost/terminalScreen.ts +26 -7
- package/server/backends/thumbnailStore.ts +5 -2
- package/server/cli-google.ts +1 -1
- package/server/config/app-config.ts +16 -0
- package/server/config/config-routes.ts +7 -0
- package/server/config/config-schema.ts +8 -0
- package/server/config/dir-config.ts +10 -2
- package/server/config/header-title.ts +7 -1
- package/server/files/pathContainment.ts +5 -1
- package/server/git/worktree-pr.ts +3 -3
- package/server/git/worktree-routes.ts +1 -1
- package/server/index.ts +127 -20
- package/server/infra/collection-tool.ts +1 -1
- package/server/infra/gui-mcp-registration.ts +90 -11
- package/server/infra/has-binary.ts +162 -14
- package/server/infra/jsonl-file.ts +88 -0
- package/server/infra/plugins-registry.ts +10 -1
- package/server/infra/resolve-bin.ts +3 -3
- package/server/infra/spawn-cwd.ts +46 -0
- package/server/infra/spawnCapture.ts +1 -1
- package/server/infra/tmux.ts +41 -0
- package/server/infra/web-push.ts +1 -1
- package/server/routes/app-routes.ts +24 -7
- package/server/routes/drop-routes.ts +50 -0
- package/server/routes/gui-mcp-routes.ts +5 -3
- package/server/routes/hook-routes.ts +26 -9
- package/server/routes/mcp-routes.ts +12 -2
- package/server/routes/plugin-routes.ts +11 -5
- package/server/routes/session-routes.ts +47 -2
- package/server/routes/ws-routes.ts +116 -39
- package/server/session/activity-transition.ts +9 -4
- package/server/session/cleared-transcripts.ts +118 -0
- package/server/session/completion-hooks.ts +51 -0
- package/server/session/cost.ts +23 -10
- package/server/session/decision-scan.ts +2 -10
- package/server/session/early-frames.ts +39 -0
- package/server/session/hook-settings.ts +1 -1
- package/server/session/last-turn.ts +6 -1
- package/server/session/launch-choice.ts +2 -2
- package/server/session/launcher-gui-mcp.ts +55 -0
- package/server/session/lifecycle.ts +17 -1
- package/server/session/mcp-config.ts +46 -2
- package/server/session/provider-env.ts +2 -2
- package/server/session/pty-connection.ts +12 -3
- package/server/session/pty-exit-log.ts +92 -0
- package/server/session/pty-spawn.ts +114 -10
- package/server/session/registry.ts +144 -37
- package/server/session/session-detail-view.ts +28 -6
- package/server/session/session-drops.ts +174 -0
- package/server/session/{dev-terminal-sessions.ts → session-id-log.ts} +16 -15
- package/server/session/session-list.ts +15 -4
- package/server/session/session-memos.ts +72 -0
- package/server/session/session-reads.ts +66 -54
- package/server/session/session-settings.ts +27 -4
- package/server/session/session-title.ts +25 -9
- package/server/session/session-tool-groups.ts +2 -2
- package/server/session/sessionListTitle.ts +13 -4
- package/server/session/spawn-claude.ts +59 -20
- package/server/session/spawn-codex.ts +31 -12
- package/server/session/spawn-shell.ts +10 -5
- package/server/session/summary-scan.ts +93 -0
- package/server/session/task-push.ts +10 -4
- package/server/session/taskPushRules.ts +5 -4
- package/server/session/terminal-replay.ts +15 -0
- package/server/session/tool-hook.ts +3 -3
- package/server/session/tool-store.ts +17 -19
- package/server/session/transcript.ts +35 -22
- package/server/session/types.ts +5 -3
- package/server/skills/mulmoterminal-bug-report/faq.md +6 -0
- package/server/skills/mulmoterminal-config/SKILL.md +34 -0
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-F7APysNp.js +0 -1
- package/dist/assets/channel-7wUqSdoX-o7hjueM_.js +0 -1
- package/dist/assets/chunk-FWX5IMBZ-DHLSFw1H-BtzzUMmo.js +0 -2
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-dAb2Nqej.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-dAb2Nqej.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CcNtA1jr.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CWY7xW6B.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BQbNqK8I.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DndYDaiz.js +0 -1
- package/dist/assets/index-BLn_JWl1.js +0 -620
- package/dist/assets/index-BZb3UNXf.css +0 -1
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-Ch8HtmdN.js +0 -1
- package/dist/assets/lib-BkHCzFsd.js +0 -9
- package/dist/assets/packet-7NZHBO7P-CQI3flND-kVLgbrf9.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-D1l75ZW4.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CgFu134o.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-_koCxt7R.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-y8_SdxMu.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CREWWR2J.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DkxX3rOi.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-COjV9W1k.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CEARqU8w.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-CrGq3OoF.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C7DIaiQ8.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-WP0WCRL-.js +0 -1
- package/server/backends/remoteHost/resilientRunner.ts +0 -174
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ color-coded so you see at a glance which are **working**, which **need you**, an
|
|
|
24
24
|
ping to your phone when a task finishes. One `npx` command, no Electron, no config.
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
npx mulmoterminal # starts on http://localhost:34567 and opens your browser
|
|
27
|
+
npx mulmoterminal@latest # starts on http://localhost:34567 and opens your browser
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
> **Something looks wrong?** Type `/mulmoterminal-bug-report` in any MulmoTerminal session. The
|
|
@@ -86,11 +86,20 @@ output you haven't seen) — driven by Claude/Codex activity hooks the server in
|
|
|
86
86
|
*Besides the grid there's a **single view** for focusing on one agent: the conversation/terminal on the left, and a **GUI panel** ("Canvas") on the right where the agent's tool calls render as documents, forms, charts, images, and HTML — not just printed text. Switch between the two with the chat / grid icons in the toolbar. **The app opens on the grid** (`/`); the single view has its own URL, `/chat`, so you can bookmark either.*
|
|
87
87
|
|
|
88
88
|
**Inserting a file path** — like a native terminal, you can put a file's absolute path into
|
|
89
|
-
the prompt: **drag a file** onto the terminal
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
the prompt: **drag a file** onto the terminal, or click the **file button** in the terminal
|
|
90
|
+
header, which asks the local server to open the OS file dialog and inserts the chosen path. The
|
|
91
|
+
path is inserted at the cursor — it is not submitted, so you can review it first.
|
|
92
|
+
|
|
93
|
+
A drag inserts the file's **own** path where the browser exposes one via `file://`
|
|
94
|
+
(Firefox/Safari), so editing it afterwards edits the file you dropped. Where the browser
|
|
95
|
+
withholds it — **Chrome**, and every browser when MulmoTerminal is open **from another
|
|
96
|
+
machine**, where a local path would name nothing on the host — the file's bytes are sent
|
|
97
|
+
instead, saved to a private per-session directory under the OS temp dir, and *that* path is
|
|
98
|
+
inserted. The session is granted that directory at launch (Claude Code's `--add-dir`, bind-mounted
|
|
99
|
+
in the sandbox too), so the agent reads it without a permission prompt; the copies are removed
|
|
100
|
+
when the session ends, and any left by a crash are swept at the next start. Up to 110 MiB per
|
|
101
|
+
file — the same ceiling as a phone attachment. **A session already running when you upgrade
|
|
102
|
+
was launched without that grant**, so drops into it still prompt; new sessions don't.
|
|
94
103
|
|
|
95
104
|
<a id="clicking-a-file-path"></a>
|
|
96
105
|
|
|
@@ -129,6 +138,51 @@ dotfiles are server-only. The 45 extensions both sides agree on live in
|
|
|
129
138
|
|
|
130
139
|
---
|
|
131
140
|
|
|
141
|
+
## What people say after switching
|
|
142
|
+
|
|
143
|
+
> These are experiences reported by users who moved over from an IDE or a split terminal —
|
|
144
|
+
> not benchmarks, and not claims we measured. Your setup may differ.
|
|
145
|
+
|
|
146
|
+
### "It stopped eating my memory"
|
|
147
|
+
|
|
148
|
+
Keeping several agents apart by opening several IDE windows is expensive: each one brings its own
|
|
149
|
+
editor, language server, extensions and file watchers. One user reported a **64 GB machine
|
|
150
|
+
stuttering** under that load, and running smoothly after moving over — here the agents are PTYs on
|
|
151
|
+
a server and the UI is browser tabs.
|
|
152
|
+
|
|
153
|
+
### "I stopped answering the wrong agent"
|
|
154
|
+
|
|
155
|
+
Six panes of scrolling text look identical. Users have described **typing a reply into another
|
|
156
|
+
agent's terminal**, and losing track of what they had asked in the first place. As one put it, the
|
|
157
|
+
windows all look the same, so switching between them costs time just to work out what you are
|
|
158
|
+
looking at.
|
|
159
|
+
|
|
160
|
+
The problem isn't attention — it's that N identical panes means holding N contexts in your head.
|
|
161
|
+
Colour-coded state, a name badge and a per-directory colour move that onto the screen instead.
|
|
162
|
+
|
|
163
|
+
### "Watching many and reading one stopped being a trade-off"
|
|
164
|
+
|
|
165
|
+
Splitting a terminal six ways leaves every pane too small to read a long answer without constant
|
|
166
|
+
scrolling and resizing — one user described exactly that with a 4,000-character reply. So you
|
|
167
|
+
quietly accept worse reading every time you add an agent.
|
|
168
|
+
|
|
169
|
+
**Grid ↔ enlarge removes that.** Watch all of them, then blow one up and read it properly — the
|
|
170
|
+
cockpit roster keeps the rest in view as text while you do.
|
|
171
|
+
|
|
172
|
+
### "My existing sessions came with me"
|
|
173
|
+
|
|
174
|
+
Sessions resume as-is — same `claude --resume`, same transcripts. Point it at a directory you
|
|
175
|
+
already work in and your history is there. Nothing to migrate, nothing to redo. One user said this
|
|
176
|
+
alone made the switch worth it, having previously lost context to killed sessions.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
**You don't need ten agents for this to pay off.** Users have reported the switch being worth it at
|
|
181
|
+
**one to three** parallel sessions. The wins above are about not losing track, not about running
|
|
182
|
+
more.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
132
186
|
## Install & run
|
|
133
187
|
|
|
134
188
|
Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
@@ -146,17 +200,17 @@ Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
|
146
200
|
|
|
147
201
|
The server starts without any of the non-required rows; you just lose that row's feature,
|
|
148
202
|
and the header/panel for it says so. `git` and `gh` are marked required because losing them
|
|
149
|
-
costs whole views rather than one button. `npx mulmoterminal init` (below) reports which of
|
|
203
|
+
costs whole views rather than one button. `npx mulmoterminal@latest init` (below) reports which of
|
|
150
204
|
these it can find.
|
|
151
205
|
|
|
152
206
|
```bash
|
|
153
|
-
npx mulmoterminal # start on http://localhost:34567 and open the browser
|
|
207
|
+
npx mulmoterminal@latest # start on http://localhost:34567 and open the browser
|
|
154
208
|
# or install globally:
|
|
155
209
|
npm install -g mulmoterminal
|
|
156
210
|
mulmoterminal
|
|
157
211
|
```
|
|
158
212
|
|
|
159
|
-
**First-run setup (optional).** `npx mulmoterminal init` checks your environment (Node ≥ 22.9
|
|
213
|
+
**First-run setup (optional).** `npx mulmoterminal@latest init` checks your environment (Node ≥ 22.9
|
|
160
214
|
and every CLI in the table above), seeds the launcher's **directory
|
|
161
215
|
presets** from the projects in your Claude Code history, and writes `~/.mulmoterminal/config.json`.
|
|
162
216
|
It's **idempotent** — re-run it any time to refresh the presets; it overwrites the managed parts
|
|
@@ -166,7 +220,7 @@ and keeps your other settings. When `claude` is installed it can hand off to the
|
|
|
166
220
|
**Google account (optional).** Link a Google account to enable the chat's `google` tool and the
|
|
167
221
|
phone's `google.calendar.*` commands: read/create events on any calendar (not just your primary),
|
|
168
222
|
list the calendars you've subscribed to, and read the colour palettes. Sign in from
|
|
169
|
-
**Settings → Google account**, or run `npx mulmoterminal google login` — the CLI is the fallback
|
|
223
|
+
**Settings → Google account**, or run `npx mulmoterminal@latest google login` — the CLI is the fallback
|
|
170
224
|
for when you're driving MulmoTerminal from another machine, since consent finishes on a loopback
|
|
171
225
|
listener and needs a browser **on the host**. Either way it needs a Desktop OAuth client JSON saved
|
|
172
226
|
as `~/.secrets/client_secret_*.json`; the refresh token lands in `~/.config/mulmo/google-token.json`
|
|
@@ -200,11 +254,11 @@ directory you run the command from), `--port <n>` (default 34567), `--no-open`,
|
|
|
200
254
|
`--version`, `--help`.
|
|
201
255
|
|
|
202
256
|
```bash
|
|
203
|
-
npx mulmoterminal --cwd ./my-project # work in a specific directory
|
|
257
|
+
npx mulmoterminal@latest --cwd ./my-project # work in a specific directory
|
|
204
258
|
```
|
|
205
259
|
|
|
206
260
|
The published package ships the server (run via `tsx`) plus the pre-built web UI;
|
|
207
|
-
`npx mulmoterminal` checks for the `claude` CLI, picks a free port, starts the
|
|
261
|
+
`npx mulmoterminal@latest` checks for the `claude` CLI, picks a free port, starts the
|
|
208
262
|
server, and opens the browser. For local development from a clone, see
|
|
209
263
|
[Running](#running).
|
|
210
264
|
|
|
@@ -426,7 +480,7 @@ Requires **Node ≥ 22.9** (uses `node --env-file-if-exists`) and the `claude` C
|
|
|
426
480
|
## Configuration
|
|
427
481
|
|
|
428
482
|
The server is configured entirely through environment variables, optionally
|
|
429
|
-
loaded from a `.env` file. `npx mulmoterminal` reads the `.env` **in the
|
|
483
|
+
loaded from a `.env` file. `npx mulmoterminal@latest` reads the `.env` **in the
|
|
430
484
|
directory you run it from**; the npm scripts read the one in the repo root. The
|
|
431
485
|
`.env` is optional — every variable below has a default, so the server runs
|
|
432
486
|
without one.
|
|
@@ -441,7 +495,7 @@ the `claude` / `codex` sessions themselves.
|
|
|
441
495
|
| `PORT` | `34567` | Backend HTTP/WebSocket port (prod: the URL you open). |
|
|
442
496
|
| `CLIENT_PORT` | `6856` | Vite dev-server port (dev only: the URL you open with `yarn dev`). |
|
|
443
497
|
| `CLAUDE_BIN` | `claude` | The Claude Code binary to spawn. On Windows a bare name is resolved on `PATH` before it reaches the PTY layer (which matches file names exactly): to the `.exe` when there is one, otherwise to the `.cmd` shim an npm-global install leaves, run through `cmd.exe`. |
|
|
444
|
-
| `CLAUDE_CWD` | current dir | Working directory each `claude` PTY runs in; determines which project's sessions the sidebar lists. Via `npx mulmoterminal` it defaults to the directory you ran the command from (override with `--cwd <dir>`, relative allowed); when the server is run directly it falls back to `~/mulmoclaude`. A value read from `.env` must be an absolute path (`~` is not expanded). |
|
|
498
|
+
| `CLAUDE_CWD` | current dir | Working directory each `claude` PTY runs in; determines which project's sessions the sidebar lists. Via `npx mulmoterminal@latest` it defaults to the directory you ran the command from (override with `--cwd <dir>`, relative allowed); when the server is run directly it falls back to `~/mulmoclaude`. A value read from `.env` must be an absolute path (`~` is not expanded). |
|
|
445
499
|
| `CLAUDE_PERMISSION_MODE` | `auto` | Permission mode passed to each `claude` spawn. |
|
|
446
500
|
| `MT_TITLE_MODEL` | `haiku` | Model used for the cell header's AI title (a cheap/fast model summarizing the recent turns). Accepts a `--model` alias or a full model id. |
|
|
447
501
|
| `CODEX_BIN` | `codex` | The Codex CLI binary to spawn. |
|
|
@@ -491,6 +545,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
491
545
|
| `decisionDigest` | Keep a **Markdown digest of the decisions this project's sessions asked for**, refreshed at startup and every few hours, so an agent can read what has already been decided before asking something similar. Written to `~/.mulmoterminal/decisions/<project>.md` (never into your repository) and served to agents by the bundled `mulmoterminal-decisions` skill. **Off by default** — it is a vision-stage idea, and it writes a file that would otherwise not exist. The digest holds dated facts, never inferred rules. |
|
|
492
546
|
| `issueWorkComments` | Let a cell **comment on the issue it is working on**: once when it starts, and again when its PR merges (closing the issue if GitHub has not already). The comment names the working **directory** it happened in — the folder name only, never the path — so a reader can tell which clone. **Off by default**; it writes to GitHub, often on somebody else's issue. Needs `gh` logged in. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#issue-work-comments). |
|
|
493
547
|
| `prWorkdirFooter` | Ends a PR body with `work in <clone>` — the directory name of the clone the work happened in, so a PR says which of several side-by-side checkouts produced it. Applies to **both** paths that open PRs here: **⧉ Open PR** appends it to the PR it creates, and every Claude session is told to end the bodies it writes with the same line (the name is resolved by the server, so a session inside a managed worktree still names the main checkout). **On by default**; set `false` to opt out — read per PR and per session spawn, so no restart is needed (there is no Settings control for it). Appending is idempotent: an existing PR never gets a second copy. |
|
|
548
|
+
| `appendSystemPrompt` | Whether a spawned Claude session is asked to end a reply with a **closing summary** — what was asked, what was achieved, what was not (see [Closing summary](#closing-summary)). **On by default**; set `false` to opt out, and a directory's `.mulmoterminal.json` outranks this. Read per spawn, so no restart is needed (there is no Settings control for it), though a session already running keeps what it was launched with. `true` / `false` only. |
|
|
494
549
|
| `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. No Settings UI: edit the file, then **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
|
|
495
550
|
|
|
496
551
|
Every MulmoTerminal on the machine shares this one file, so an older build could save over a key a
|
|
@@ -613,7 +668,8 @@ malformed file is ignored.
|
|
|
613
668
|
"fontFamily": "'Cica', monospace", // terminal font stack; overrides the global config
|
|
614
669
|
"orderPriority": 10, // rank in the grid's "priority" ordering (lowest first)
|
|
615
670
|
"sound": "./.mulmoterminal/alert.mp3", // attention sound, RELATIVE to this directory
|
|
616
|
-
"sounds": { "command-failed": "preset:gong" } // per-notification-kind override
|
|
671
|
+
"sounds": { "command-failed": "preset:gong" }, // per-notification-kind override
|
|
672
|
+
"appendSystemPrompt": false // no closing summary here; omit to follow the global setting
|
|
617
673
|
}
|
|
618
674
|
```
|
|
619
675
|
|
|
@@ -638,6 +694,7 @@ malformed file is ignored.
|
|
|
638
694
|
| `fontFamily` | CSS font-family stack for this directory's terminals, overriding the global `fontFamily`. Use the names as your OS lists them (`"'Cica', 'MS Gothic', monospace"`). An unusable stack is ignored whole rather than half-applied; `monospace` is appended if you name no generic family. Prefer fonts whose fullwidth glyphs are exactly twice the Latin width, or box-drawing frames tear. |
|
|
639
695
|
| `sound` | Attention sound for this directory's sessions, a path **relative to the directory** (served at `GET /api/dir-sound`). The fallback for every kind. |
|
|
640
696
|
| `sounds` | Per-kind override of `sound`: `{ "command-failed": "preset:gong" }`. Each value is a `preset:<id>` or a directory-relative path, under the same confinement. |
|
|
697
|
+
| `appendSystemPrompt` | Whether this directory's Claude sessions are asked to end a reply with a **closing summary** (see [Closing summary](#closing-summary)). Omit to follow the global `appendSystemPrompt`, which is on; `true` / `false` here outranks it. Read per spawn, so a new session in this directory picks up an edit without a restart. |
|
|
641
698
|
| `addDirs` | Extra directories this project's Claude sessions may read and edit — the terminal-side equivalent of opening several folders in one VS Code workspace, via Claude Code's `--add-dir`. Relative entries resolve against **this file's directory** (`"../shared-lib"`), a path that doesn't exist is dropped, max 16. In the Docker sandbox each one is bind-mounted too, so the grant is real inside the container — which widens the sandbox on purpose. Claude only: codex has no equivalent flag and ignores the key. |
|
|
642
699
|
|
|
643
700
|
**Security.** `sound` and every `sounds` entry are directory-relative paths only — absolute
|
|
@@ -1191,6 +1248,7 @@ same-origin-guarded.
|
|
|
1191
1248
|
| `POST /api/translation` | Runtime UI-string translation. |
|
|
1192
1249
|
| `GET /api/remote-host/status` · `POST /api/remote-host/{connect,disconnect}` | Companion phone-client link. Each response carries the command channel's `health` (`online` / `reconnecting` / `offline`, plus the last listener error), so the toolbar shows a dropped channel instead of the last state it happened to fetch. |
|
|
1193
1250
|
| `POST /api/open-dir` · `POST /api/pick-file` | Reveal a dir in Finder/Explorer; OS file-picker → path (`{ directory: true }` opens the folder picker — used by the launcher's Working-directory 📁 button). |
|
|
1251
|
+
| `POST /api/session/:id/drop` | A dropped file whose path the browser withheld. **Raw bytes**, not JSON, under the file's own content type (base64 in JSON would cap real files near 18 MB, and a dropped `.json` would be parsed as a document); the original name rides percent-encoded in `x-drop-filename` and is used for its **suffix only**. Answers `{ path }` — absolute, inside the private per-session directory the session was granted at launch. 110 MiB cap; 404 for a session this server isn't running. |
|
|
1194
1252
|
|
|
1195
1253
|
The phone itself uses **none** of these routes — it reaches the host over Firestore command
|
|
1196
1254
|
docs, not HTTP. Every command it can send, and the shapes it gets back, are in
|
|
@@ -1359,6 +1417,16 @@ Key rules:
|
|
|
1359
1417
|
- Brand-new sessions appear in the sidebar **immediately** (before their `.jsonl`
|
|
1360
1418
|
exists) via the in-memory `knownSessions` registry + a `created` push; an
|
|
1361
1419
|
unused one disappears when its PTY is reaped.
|
|
1420
|
+
- **Background workers get their own filter.** A session nobody started by hand —
|
|
1421
|
+
a collection's scheduled refresh, or a plugin's `spawnBackgroundChat`
|
|
1422
|
+
`hidden: true` — is listed under the **Background** chip instead of among the
|
|
1423
|
+
chats, so a refresh schedule doesn't fill the history. It stays openable (a
|
|
1424
|
+
MulmoTerminal session is a live terminal, so a row you can't reach is a process
|
|
1425
|
+
you can't stop), and it is put on the same count+age retention as the
|
|
1426
|
+
scheduler's own sessions. The chip appears only when there is one to show. A
|
|
1427
|
+
**manual** collection Refresh is a normal visible session — unchanged. The
|
|
1428
|
+
marking is persisted (`~/.mulmoterminal/background-sessions.json`), so a worker
|
|
1429
|
+
stays out of the chat list after it finishes and after a restart.
|
|
1362
1430
|
|
|
1363
1431
|
---
|
|
1364
1432
|
|
|
@@ -1400,7 +1468,17 @@ placed last with nothing after it.
|
|
|
1400
1468
|
|
|
1401
1469
|
It is deliberately **not** written on every turn: mid-work replies and short factual answers
|
|
1402
1470
|
carry no standing request, and a summary that always appears stops being read. The wording
|
|
1403
|
-
lives in `server/agents/session-summary-prompt.ts
|
|
1471
|
+
lives in `server/agents/session-summary-prompt.ts`.
|
|
1472
|
+
|
|
1473
|
+
**On by default, and switchable off** with `appendSystemPrompt: false` — in
|
|
1474
|
+
`~/.mulmoterminal/config.json`, or in a directory's `.mulmoterminal.json`, which outranks the
|
|
1475
|
+
global value. Read per spawn, so no restart is needed; a session already running keeps what it
|
|
1476
|
+
was launched with. Nothing in the app parses what the summary says, so turning it off costs no
|
|
1477
|
+
feature — the roster and push notifications simply show the raw tail of the reply.
|
|
1478
|
+
|
|
1479
|
+
Which sections `--append-system-prompt` ends up carrying is decided in
|
|
1480
|
+
`server/agents/appended-prompt.ts`: this one and the `prWorkdirFooter` clone line are separate
|
|
1481
|
+
settings on the same flag, and with both off the flag is not passed at all.
|
|
1404
1482
|
|
|
1405
1483
|
Passed inline rather than as `--append-system-prompt-file` for the same reason `--settings`
|
|
1406
1484
|
is: the sandbox spawn runs in a container that cannot read a host path.
|
|
@@ -1419,12 +1497,13 @@ has its `/` and `.` characters replaced with `-` (e.g.
|
|
|
1419
1497
|
A session's display **title** is derived by scanning its JSONL for, in order of
|
|
1420
1498
|
preference:
|
|
1421
1499
|
|
|
1422
|
-
1.
|
|
1423
|
-
2. else
|
|
1424
|
-
3. else the latest `
|
|
1425
|
-
4. else the
|
|
1500
|
+
1. the **session note** the user wrote (see below),
|
|
1501
|
+
2. else a live **AI title** the server generated for the session this run (see below),
|
|
1502
|
+
3. else the latest `ai-title` record's `aiTitle` (e.g. written by MulmoClaude),
|
|
1503
|
+
4. else the latest `last-prompt` record's `lastPrompt`,
|
|
1504
|
+
5. else the first real user message (slash/local-command wrappers like
|
|
1426
1505
|
`<local-command-…>` are skipped),
|
|
1427
|
-
|
|
1506
|
+
6. else `"(untitled session)"`.
|
|
1428
1507
|
|
|
1429
1508
|
In-memory sessions not yet persisted show as `"New session"` until their file
|
|
1430
1509
|
appears, at which point the on-disk title takes over.
|
|
@@ -1444,6 +1523,23 @@ ack (so the raw last prompt would be stale), or every few turns to keep a long s
|
|
|
1444
1523
|
title current. The title lives in memory (never written into Claude's own transcript); a
|
|
1445
1524
|
resumed session falls back to any on-disk `ai-title`.
|
|
1446
1525
|
|
|
1526
|
+
### Session note
|
|
1527
|
+
|
|
1528
|
+
Every tier above says what the **agent** said, which stops answering "which cell is this?"
|
|
1529
|
+
once several sessions are open. So a cell header also takes a **note you write yourself**: the
|
|
1530
|
+
pencil button beside the header text opens a one-line box (Enter saves, Esc cancels, clicking
|
|
1531
|
+
away saves). While a note is set it *replaces* the header line — the title it displaced stays in
|
|
1532
|
+
the tooltip — and it becomes the session's title in the sidebar list and on the phone's roster
|
|
1533
|
+
too, so one session goes by one name everywhere.
|
|
1534
|
+
|
|
1535
|
+
Notes are capped at 200 characters and folded to a single line. They are stored per **session
|
|
1536
|
+
id** in `~/.mulmoterminal/session-memos.jsonl` and survive both the session being reaped and a
|
|
1537
|
+
server restart: resume the session and the note comes back. Saving one publishes it on the
|
|
1538
|
+
`sessions` channel, so every other open tab and the phone update without asking.
|
|
1539
|
+
|
|
1540
|
+
`POST /api/session/:id/memo` with `{ "text": "…" }` writes one; an empty `text` erases it. The
|
|
1541
|
+
route answers with the **stored** text, which is what a reload will show.
|
|
1542
|
+
|
|
1447
1543
|
---
|
|
1448
1544
|
|
|
1449
1545
|
## Project structure
|
package/bin/cli-args.d.ts
CHANGED
|
@@ -11,3 +11,8 @@ export declare const MIN_NODE_LABEL: string;
|
|
|
11
11
|
export declare function nodeMeetsMinimum(version: string): boolean;
|
|
12
12
|
export declare function serverNodeArgs(serverEntry: string, launchDir: string): string[];
|
|
13
13
|
export declare function serverSpawnEnv(env: Record<string, string | undefined>, port: number, cwd: string): Record<string, string | undefined>;
|
|
14
|
+
export interface RunningInstance {
|
|
15
|
+
pid: number;
|
|
16
|
+
port: number | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function runningInstancesPrompt(instances: readonly RunningInstance[]): string;
|
package/bin/cli-args.js
CHANGED
|
@@ -108,6 +108,26 @@ export function saysYes(answer) {
|
|
|
108
108
|
* updates never cross — so the instance that did not start a session shows a frozen history
|
|
109
109
|
* for it (#611).
|
|
110
110
|
*/
|
|
111
|
+
/**
|
|
112
|
+
* The question asked when another server is ALREADY RUNNING, whatever port this one was told to
|
|
113
|
+
* use (#1061).
|
|
114
|
+
*
|
|
115
|
+
* `secondInstancePrompt` below only fires when the wanted port is taken, so `--port <free>`
|
|
116
|
+
* started a second instance in silence — which is how eight live sessions had their settings
|
|
117
|
+
* deleted by a peer's boot. The clash is a symptom; the shared `~/.mulmoterminal` is the thing
|
|
118
|
+
* that is not supported, and that is true at any port.
|
|
119
|
+
*/
|
|
120
|
+
export function runningInstancesPrompt(instances) {
|
|
121
|
+
const where = instances.map((i) => (i.port === null ? `pid ${i.pid}` : `http://localhost:${i.port}`)).join(", ");
|
|
122
|
+
const subject = instances.length === 1 ? "MulmoTerminal is already running" : `${instances.length} MulmoTerminal servers are already running`;
|
|
123
|
+
return [
|
|
124
|
+
`${subject} (${where}).`,
|
|
125
|
+
" Running more than one is NOT a supported setup: they share ~/.mulmoterminal,",
|
|
126
|
+
" so they can overwrite each other's session state.",
|
|
127
|
+
"Start another one anyway? [y/N] ",
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
111
131
|
export const SECOND_INSTANCE_NOTE = [
|
|
112
132
|
" Note: both share ~/.mulmoterminal. A session's tool history does not live-update",
|
|
113
133
|
" in the instance that did not start it.",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface InstanceEntry {
|
|
2
|
+
pid: number;
|
|
3
|
+
port: number | null;
|
|
4
|
+
startedAt: number | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function instancesDir(): string;
|
|
7
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
8
|
+
export declare function registerInstance(port: number, pid?: number, startedAt?: number): () => void;
|
|
9
|
+
export declare function liveInstances(excludePid?: number): InstanceEntry[];
|
|
10
|
+
export declare function earliestStartedAt(instances: readonly InstanceEntry[]): number | null;
|
package/bin/instances.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Which MulmoTerminal servers are running right now (#1061).
|
|
2
|
+
//
|
|
3
|
+
// Two things need this answer and neither could get it before. The launcher asks so it can say
|
|
4
|
+
// "one is already running" no matter which port the new one was told to use — the port probe only
|
|
5
|
+
// notices a clash on the SAME port, so `--port <free>` started a second instance in silence. And
|
|
6
|
+
// the server asks before deleting orphaned session settings: without tmux, "nothing survived the
|
|
7
|
+
// last lifetime" is only true for the process that OWNED that lifetime, so a second instance was
|
|
8
|
+
// reading a peer's live files as leftovers and removing them.
|
|
9
|
+
//
|
|
10
|
+
// Shared as plain JS rather than through common/ because the launcher runs on bare node before
|
|
11
|
+
// any TypeScript exists — the same reason bin/update-check.js is shaped this way, and the server
|
|
12
|
+
// imports it the same way.
|
|
13
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
export const instancesDir = () => path.join(homedir(), ".mulmoterminal", "instances");
|
|
18
|
+
|
|
19
|
+
const entryFile = (pid) => path.join(instancesDir(), `${pid}.json`);
|
|
20
|
+
|
|
21
|
+
/** Whether a pid belongs to a process that still exists. Signal 0 performs the permission and
|
|
22
|
+
* existence checks without delivering anything; EPERM means it IS there, just not ours. */
|
|
23
|
+
export function isProcessAlive(pid) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return err?.code === "EPERM";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Announce this process, and hand back the removal. Best-effort throughout: not being able to
|
|
34
|
+
* say hello is not a reason to fail a boot, and a crash simply leaves an entry whose pid no
|
|
35
|
+
* longer resolves — which every reader already has to handle. */
|
|
36
|
+
export function registerInstance(port, pid = process.pid, startedAt = Date.now()) {
|
|
37
|
+
const file = entryFile(pid);
|
|
38
|
+
try {
|
|
39
|
+
mkdirSync(instancesDir(), { recursive: true, mode: 0o700 });
|
|
40
|
+
// Written elsewhere and renamed into place: a reader that catches us mid-write would see a
|
|
41
|
+
// truncated entry, and "I could not parse it" must never be how a LIVE peer disappears from
|
|
42
|
+
// the registry (Codex review). rename is atomic for a reader on the same directory.
|
|
43
|
+
const tmp = `${file}.${startedAt}.tmp`;
|
|
44
|
+
writeFileSync(tmp, JSON.stringify({ pid, port, startedAt }), { encoding: "utf8", mode: 0o600 });
|
|
45
|
+
renameSync(tmp, file);
|
|
46
|
+
} catch {
|
|
47
|
+
return () => {};
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
try {
|
|
51
|
+
rmSync(file, { force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
// Left behind; the next reader drops it as stale.
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parseEntry = (raw) => {
|
|
59
|
+
try {
|
|
60
|
+
const entry = JSON.parse(raw);
|
|
61
|
+
if (!entry || typeof entry !== "object") return null;
|
|
62
|
+
const { pid, port, startedAt } = entry;
|
|
63
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
64
|
+
return { pid, port: Number.isInteger(port) ? port : null, startedAt: Number.isFinite(startedAt) ? startedAt : null };
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Every OTHER running server, newest entry first. Entries whose process is gone are deleted as
|
|
71
|
+
* they are found: a crash cannot clean up after itself, and leaving them would make a lone
|
|
72
|
+
* instance believe it has company forever. */
|
|
73
|
+
export function liveInstances(excludePid = process.pid) {
|
|
74
|
+
let names;
|
|
75
|
+
try {
|
|
76
|
+
names = readdirSync(instancesDir());
|
|
77
|
+
} catch {
|
|
78
|
+
return []; // never registered on this machine
|
|
79
|
+
}
|
|
80
|
+
const live = [];
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
if (!name.endsWith(".json")) continue;
|
|
83
|
+
const file = path.join(instancesDir(), name);
|
|
84
|
+
let entry;
|
|
85
|
+
try {
|
|
86
|
+
entry = parseEntry(readFileSync(file, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
continue; // being written right now, or unreadable — say nothing about it
|
|
89
|
+
}
|
|
90
|
+
// Only a positively-identified dead owner earns removal. An entry we could not parse is one
|
|
91
|
+
// we know nothing about — deleting it on that basis is how a live peer gets erased, which is
|
|
92
|
+
// the very failure this registry exists to prevent (Codex review).
|
|
93
|
+
if (entry === null) continue;
|
|
94
|
+
if (!isProcessAlive(entry.pid)) {
|
|
95
|
+
try {
|
|
96
|
+
rmSync(file, { force: true });
|
|
97
|
+
} catch {
|
|
98
|
+
// not ours to remove; harmless
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (entry.pid !== excludePid) live.push(entry);
|
|
103
|
+
}
|
|
104
|
+
return live.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** When the EARLIEST of the given instances started, or null when there are none.
|
|
108
|
+
*
|
|
109
|
+
* This is the cutoff the settings prune needs: a file written before every live peer began
|
|
110
|
+
* cannot belong to one of them, so it is safe to treat as a leftover. A file written after that
|
|
111
|
+
* moment might be a peer's, and a maybe is not good enough to delete somebody's live state. */
|
|
112
|
+
export function earliestStartedAt(instances) {
|
|
113
|
+
const times = instances.map((i) => i.startedAt).filter((t) => typeof t === "number");
|
|
114
|
+
return times.length ? Math.min(...times) : null;
|
|
115
|
+
}
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -22,12 +22,14 @@ import {
|
|
|
22
22
|
portInUseMessage,
|
|
23
23
|
saysYes,
|
|
24
24
|
secondInstancePrompt,
|
|
25
|
+
runningInstancesPrompt,
|
|
25
26
|
SECOND_INSTANCE_NOTE,
|
|
26
27
|
nodeMeetsMinimum,
|
|
27
28
|
MIN_NODE_LABEL,
|
|
28
29
|
serverNodeArgs,
|
|
29
30
|
serverSpawnEnv,
|
|
30
31
|
} from "./cli-args.js";
|
|
32
|
+
import { liveInstances } from "./instances.js";
|
|
31
33
|
|
|
32
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
35
|
const PKG_DIR = join(__dirname, "..");
|
|
@@ -155,7 +157,10 @@ async function runInit(initArgs) {
|
|
|
155
157
|
return;
|
|
156
158
|
}
|
|
157
159
|
if (hasClaude) log("Later: run `claude` in any project and use /mulmoterminal-config");
|
|
158
|
-
|
|
160
|
+
// Pinned to @latest: an unpinned `npx` reuses whatever it already has cached, so the very
|
|
161
|
+
// command printed for someone to type next would start an older version than the one they
|
|
162
|
+
// just set up with.
|
|
163
|
+
log("Setup done. Start MulmoTerminal: npx mulmoterminal@latest");
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
// `npx mulmoterminal google <command>` — Google account linking. Consent needs a
|
|
@@ -240,6 +245,20 @@ function findEphemeralPort() {
|
|
|
240
245
|
});
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
// Ask about an ALREADY-RUNNING server, whatever port this one will use. Declining exits 0: the
|
|
249
|
+
// user answered the question that was asked, which is not a failure.
|
|
250
|
+
async function confirmNoRunningInstance() {
|
|
251
|
+
const running = liveInstances();
|
|
252
|
+
if (running.length === 0) return;
|
|
253
|
+
if (!process.stdin.isTTY) {
|
|
254
|
+
log(runningInstancesPrompt(running).replace(/\nStart another one anyway\? \[y\/N\] $/, ""));
|
|
255
|
+
log(SECOND_INSTANCE_NOTE);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!(await promptYesNo(runningInstancesPrompt(running)))) process.exit(0);
|
|
259
|
+
log(SECOND_INSTANCE_NOTE);
|
|
260
|
+
}
|
|
261
|
+
|
|
243
262
|
async function choosePort(requested, explicit) {
|
|
244
263
|
if (await isPortFree(requested)) return requested;
|
|
245
264
|
// No SILENT fallback: starting a second server on another port without saying so is how
|
|
@@ -319,7 +338,7 @@ function runServer(port, noOpen, cwd, onChild) {
|
|
|
319
338
|
|
|
320
339
|
function printHelp() {
|
|
321
340
|
console.log(`
|
|
322
|
-
Usage: npx mulmoterminal [command] [options]
|
|
341
|
+
Usage: npx mulmoterminal@latest [command] [options]
|
|
323
342
|
|
|
324
343
|
Commands:
|
|
325
344
|
(none) Start the server (default)
|
|
@@ -393,6 +412,12 @@ async function main() {
|
|
|
393
412
|
// The probe above can still lose to something binding the port in the same instant, in
|
|
394
413
|
// which case the server exits 75 and runServer returns. Same answer as the probe: say who
|
|
395
414
|
// has it rather than moving to a port nobody asked for.
|
|
415
|
+
// Before anything about PORTS: is one already running at all? A clash on the same port is only
|
|
416
|
+
// the visible half — `--port <free>` used to start a second instance in silence, and the two
|
|
417
|
+
// then share ~/.mulmoterminal (#1061). Non-TTY says it and carries on rather than hanging on a
|
|
418
|
+
// prompt nobody can answer: a script that asked for a server should still get one.
|
|
419
|
+
await confirmNoRunningInstance();
|
|
420
|
+
|
|
396
421
|
const port = await choosePort(requestedPort, portExplicit);
|
|
397
422
|
await runServer(port, noOpen, cwd, (c) => {
|
|
398
423
|
child = c;
|
|
@@ -30,6 +30,7 @@ export const DIR_CONFIG_KEYS = [
|
|
|
30
30
|
"provider",
|
|
31
31
|
"model",
|
|
32
32
|
"addDirs",
|
|
33
|
+
"appendSystemPrompt",
|
|
33
34
|
] as const;
|
|
34
35
|
|
|
35
36
|
export interface DirConfigSource {
|
|
@@ -58,6 +59,10 @@ export interface DirConfigExtras {
|
|
|
58
59
|
model: string | null;
|
|
59
60
|
skills: string[] | null;
|
|
60
61
|
addDirs: string[] | null;
|
|
62
|
+
// Tri-state on purpose: `false` is a setting this file made, and the preview has to show it as
|
|
63
|
+
// one. Carried here because a boolean cannot be read back off the per-cell config the way a
|
|
64
|
+
// colour can — without it the panel reports the file as setting nothing at all.
|
|
65
|
+
appendSystemPrompt: boolean | null;
|
|
61
66
|
buttonLabels: string[];
|
|
62
67
|
chipLabels: string[];
|
|
63
68
|
}
|
|
@@ -67,6 +72,7 @@ export const EMPTY_DIR_CONFIG_EXTRAS: DirConfigExtras = {
|
|
|
67
72
|
model: null,
|
|
68
73
|
skills: null,
|
|
69
74
|
addDirs: null,
|
|
75
|
+
appendSystemPrompt: null,
|
|
70
76
|
buttonLabels: [],
|
|
71
77
|
chipLabels: [],
|
|
72
78
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The wire contract for sending a dropped file's bytes to the host, when the browser withheld
|
|
2
|
+
// its path. Both sides decide from these: the server enforces the cap and the browser refuses
|
|
3
|
+
// early rather than spending a long upload on bytes that will be rejected at the end.
|
|
4
|
+
|
|
5
|
+
// Matches the phone's attachment ingest, so the same file is accepted whichever way it arrives.
|
|
6
|
+
export const MAX_DROP_BYTES = 110 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
// A header, not a body field, because the body IS the file. Percent-encoded by the sender: a
|
|
9
|
+
// header is latin-1 and real filenames are not, so a Japanese name sent raw arrives mangled.
|
|
10
|
+
export const DROP_FILENAME_HEADER = "x-drop-filename";
|
|
11
|
+
|
|
12
|
+
// What POST /api/session/:id/drop answers with on success. `path` is absolute, inside the
|
|
13
|
+
// directory that session was granted at spawn time.
|
|
14
|
+
export interface DropUploadResponse {
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The guard both sides use before reading a NUMBER off parsed JSON. Null — never 0 — for
|
|
2
|
+
* anything that is not a real number, because the callers are gauges and costs where a missing
|
|
3
|
+
* value and a zero mean opposite things (see common/rateLimits.ts).
|
|
4
|
+
*
|
|
5
|
+
* `Number.isFinite` rather than a bare typeof: `JSON.parse` cannot produce NaN or Infinity, but
|
|
6
|
+
* these values also arrive from arithmetic on parsed fields, and NaN reaching a gauge renders as
|
|
7
|
+
* an empty bar rather than as an error. Sibling of common/isRecord.ts, which exists because the
|
|
8
|
+
* same one-liner had been hand-copied into 29 files. */
|
|
9
|
+
export const finiteNumber = (value: unknown): number | null => (typeof value === "number" && Number.isFinite(value) ? value : null);
|
package/common/quickCommands.ts
CHANGED
|
@@ -9,5 +9,7 @@ import type { SessionAgent } from "./sessionAgent.js";
|
|
|
9
9
|
export interface QuickCommand {
|
|
10
10
|
label: string;
|
|
11
11
|
text: string;
|
|
12
|
-
|
|
12
|
+
// `| undefined` because quickCommandSchema `satisfies z.ZodType<QuickCommand>`, and
|
|
13
|
+
// Zod's `.optional()` produces `T | undefined`, not "key may be absent".
|
|
14
|
+
agents?: SessionAgent[] | undefined;
|
|
13
15
|
}
|
package/common/rateLimits.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// because the plan is API-key billed, because no session has answered yet, or because upstream
|
|
9
9
|
// dropped the field (anthropics/claude-code#40094). None of those are zero, and a gauge reading
|
|
10
10
|
// 0% when the truth is 83% is the worst thing this data can do.
|
|
11
|
+
import { isRecord } from "./isRecord.js";
|
|
12
|
+
import { finiteNumber } from "./finiteNumber.js";
|
|
11
13
|
|
|
12
14
|
export interface RateLimitWindow {
|
|
13
15
|
usedPercentage: number; // 0-100, fractional
|
|
@@ -19,9 +21,6 @@ export interface RateLimits {
|
|
|
19
21
|
sevenDay: RateLimitWindow | null;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
|
|
23
|
-
const finiteNumber = (value: unknown): number | null => (typeof value === "number" && Number.isFinite(value) ? value : null);
|
|
24
|
-
|
|
25
24
|
export function parseRateLimitWindow(raw: unknown): RateLimitWindow | null {
|
|
26
25
|
if (!isRecord(raw)) return null;
|
|
27
26
|
const used = finiteNumber(raw.usedPercentage);
|
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
// reconnecting — it died and is being re-subscribed with backoff (self-healing)
|
|
7
7
|
// offline — re-subscribing stopped helping, or nothing is connected at all;
|
|
8
8
|
// recovering needs a re-auth from the browser's parked session
|
|
9
|
+
//
|
|
10
|
+
// @mulmoclaude/core declares the same three fields, and the server hands this straight to core's
|
|
11
|
+
// resilient runner — structurally, so the two cannot disagree without a type error. Kept here
|
|
12
|
+
// anyway because the BROWSER renders it: core exposes the type only from `remote-host`, whose
|
|
13
|
+
// entry imports firebase/firestore, and pulling that into the Vue bundle to describe three fields
|
|
14
|
+
// is the wrong trade. If core ever gives the health type its own subpath, delete this and import.
|
|
9
15
|
|
|
10
16
|
import { isRecord } from "./isRecord.js";
|
|
11
17
|
export const RUNNER_HEALTH_STATES = ["online", "reconnecting", "offline"] as const;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The user's own one-line note on a session, as BOTH sides agree it should look.
|
|
2
|
+
//
|
|
3
|
+
// Shared because two layers decide from it and must not drift: the input box caps what can be
|
|
4
|
+
// typed, and the server caps what it stores. A client-only cap is no cap at all — the route is
|
|
5
|
+
// reachable directly — and a server-only one silently truncates what the user was shown typing.
|
|
6
|
+
|
|
7
|
+
export const MEMO_MAX_LENGTH = 200;
|
|
8
|
+
|
|
9
|
+
// `Cc` is the C0/C1 control block exactly: the newlines, tabs and terminal escapes a paste can
|
|
10
|
+
// carry, and nothing else. `\p{C}` would also take the format category, which is where the
|
|
11
|
+
// zero-width joiner holding a multi-part emoji together lives.
|
|
12
|
+
const CONTROL_CHARS = /\p{Cc}/gu;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A memo as it is stored and shown, or "" for "there is none".
|
|
16
|
+
*
|
|
17
|
+
* "" is the ERASE value, not a memo that happens to be empty: the store deletes on it and the
|
|
18
|
+
* header falls back to the AI title. That is why whitespace-only input normalizes to "" — a
|
|
19
|
+
* user who selects all and types a space means to clear the note.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeMemo(raw: unknown): string {
|
|
22
|
+
if (typeof raw !== "string") return "";
|
|
23
|
+
const oneLine = raw.replace(CONTROL_CHARS, " ").replace(/\s+/gu, " ").trim();
|
|
24
|
+
// By code point, so the cap never lands between the halves of a surrogate pair and leaves a
|
|
25
|
+
// lone half in a JSON line that every later reader has to carry.
|
|
26
|
+
return [...oneLine].slice(0, MEMO_MAX_LENGTH).join("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The name a session goes by: the user's memo when there is one, else the best generated title
|
|
31
|
+
* on offer, else "" for the caller's own sentinel.
|
|
32
|
+
*
|
|
33
|
+
* Shared because THREE surfaces answer this question — the grid cell's header, the sidebar row
|
|
34
|
+
* and the phone's roster — and a session that goes by two different names depending on where it
|
|
35
|
+
* is looked at is worse than one with no name at all. Each caller passes its own tiers, in its
|
|
36
|
+
* own order; what lives here is the one rule they agree on, that the line the USER wrote outranks
|
|
37
|
+
* everything the agent said.
|
|
38
|
+
*
|
|
39
|
+
* Truthiness, not nullishness: an empty tier means "nothing usable here, keep looking", never
|
|
40
|
+
* "show blank".
|
|
41
|
+
*/
|
|
42
|
+
export function sessionDisplayName(memo: string | null | undefined, ...generated: (string | null | undefined)[]): string {
|
|
43
|
+
return [memo, ...generated].find((candidate) => !!candidate) ?? "";
|
|
44
|
+
}
|