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.
Files changed (236) hide show
  1. package/README.md +117 -21
  2. package/bin/cli-args.d.ts +5 -0
  3. package/bin/cli-args.js +20 -0
  4. package/bin/instances.d.ts +10 -0
  5. package/bin/instances.js +115 -0
  6. package/bin/mulmoterminal.js +27 -2
  7. package/common/dirConfigSource.ts +6 -0
  8. package/common/dropUpload.ts +16 -0
  9. package/common/finiteNumber.ts +9 -0
  10. package/common/quickCommands.ts +3 -1
  11. package/common/rateLimits.ts +2 -3
  12. package/common/remoteHostHealth.ts +6 -0
  13. package/common/sessionMemo.ts +44 -0
  14. package/common/toolGroups.ts +37 -5
  15. package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CkfTvNiT.js → abnfDiagram-VRR7QNED-RjiYivmv-i7KLnuI-.js} +1 -1
  16. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-DRxyAOZm.js +1 -0
  17. package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-DU5fnnYi.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-CL3Or8CZ.js} +1 -1
  18. package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-DJtyRfct.js → blockDiagram-677ZJIJ3-BPuAJQRW-DFu8zII3.js} +1 -1
  19. package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-CaAB4hIh.js → c4Diagram-LMCZKHZV-C0LAqQso-CByG3jFJ.js} +1 -1
  20. package/dist/assets/channel-7wUqSdoX-CryXErAP.js +1 -0
  21. package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-C2pc6s6p.js → chunk-32BRIVSS-BRrYpgtb-Dc3x0Hgh.js} +1 -1
  22. package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-BiDQ5LS-.js → chunk-52WLFC77-BJ-ss3Xr-D6efVaUm.js} +1 -1
  23. package/dist/assets/{chunk-C7G6YPKG-BZEucKEL-B6NLudrF.js → chunk-C7G6YPKG-BZEucKEL-CUdIiVHU.js} +1 -1
  24. package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-Dydj9mRL.js → chunk-EX3LRPZG-DLS6FBN1-BH94eoNV.js} +1 -1
  25. package/dist/assets/chunk-FWX5IMBZ-DHLSFw1H-Dw8tAp6e.js +2 -0
  26. package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-460OsWXy.js → chunk-HOUHSVGY-Bhlt8hXJ-CEurvfOt.js} +1 -1
  27. package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-CSp9RB18.js → chunk-ICXQ74PX-DwgHBX_g-DKrDQRbg.js} +1 -1
  28. package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-CC-mdXDP.js → chunk-MOJQB5TN-CMZRaeqt-Ch-WBypm.js} +1 -1
  29. package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-nrJtDZ0I.js → chunk-OGEWGWER-8Qy4a8b5-BfJ1_Ye1.js} +1 -1
  30. package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-D7s7UkTC.js → chunk-PUDLZKDR-DcrWQRYh-NggLAE5k.js} +1 -1
  31. package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-Vjuup2uc.js → chunk-Q4XR5HBZ-ZXVGkG8Z-DmtrERIM.js} +1 -1
  32. package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-V1zSMg_1.js → chunk-V7JOEXUC-DmGdheTX-C8lSwSR3.js} +1 -1
  33. package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-C1PM97Ei.js → chunk-VAUOI2AC-CS9QJ4yz-DNlwzQL1.js} +1 -1
  34. package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-zf8Ln3gQ.js → chunk-VR4S4FIN-C6a91eNY-DX2Te6ga.js} +1 -1
  35. package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CgOwOaF2.js → chunk-WYO6CB5R-BlzOfotS-BAOXGFjf.js} +1 -1
  36. package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-C17Ongqc.js → chunk-ZGVPDNZ5-BYwxNFTK-CS40JTYJ.js} +1 -1
  37. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-Bh9HKUjT.js +1 -0
  38. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-Bh9HKUjT.js +1 -0
  39. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CNhjzHRd.js +1 -0
  40. package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--rvKNlp57.js → cynefinDiagram-TSTJHNR4-DHA9iPo--DUt8F5SJ.js} +1 -1
  41. package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-DAPicxb0.js → dagre-VKFMJZFB--oJKqXBZ-BhcFRdM7.js} +1 -1
  42. package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-Cwk0uscZ.js → diagram-FQU43EPY-CcJCB9bG-CBj7RNID.js} +1 -1
  43. package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-CLLr3WWX.js → diagram-G47NLZAW-C_o-WGG1-DCAfQJQ2.js} +1 -1
  44. package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--YbeWvGrQ.js → diagram-NH7WQ7WH-CXJCYvY--133061sd.js} +1 -1
  45. package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-CCzLGMoX.js → diagram-OA4YK3LP-BMzeJ87A-DPt25PXB.js} +1 -1
  46. package/dist/assets/{diagram-WEI45ONY-D_93NKqo-C_vctp-i.js → diagram-WEI45ONY-D_93NKqo-Bb2DJ3v3.js} +1 -1
  47. package/dist/assets/dist-4eLWDv4V.js +11 -0
  48. package/dist/assets/dist-BmZVepIx.js +2 -0
  49. package/dist/assets/dist-CDpjkjbB.js +9 -0
  50. package/dist/assets/dist-CXRReMvS.js +1 -0
  51. package/dist/assets/dist-Ci7pnEU6.js +11 -0
  52. package/dist/assets/dist-Do1AxSN7.js +1 -0
  53. package/dist/assets/dist-DyY2Odgu.js +1 -0
  54. package/dist/assets/dist-FzB0wwv6.js +1 -0
  55. package/dist/assets/dist-Lnp6Clqz.js +1 -0
  56. package/dist/assets/dist-berHSMWa.js +1 -0
  57. package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-C9RE0jib.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-CqvyVeoa.js} +1 -1
  58. package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-DNe_dsG7.js → erDiagram-Q63AITRT-CiABnA0s-jrNyKW5P.js} +1 -1
  59. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-BzmuvEpa.js +1 -0
  60. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-D_olhK5u.js +1 -0
  61. package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-D0fBB7vz.js → ganttDiagram-NO4QXBWP-DQZvdFo1-Dz1MAsy4.js} +1 -1
  62. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-6E2-f_ab.js +1 -0
  63. package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-_9JEvhUj.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-CV3qv2yf.js} +1 -1
  64. package/dist/assets/index-CbJGZlpM.js +616 -0
  65. package/dist/assets/index-Zds0SlQi.css +1 -0
  66. package/dist/assets/info-DKCQHKI2-Dplx5kMp-DxJGF13X.js +1 -0
  67. package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-sf0LZ0TR.js → infoDiagram-FWYZ7A6U-7UnoB5AP-BZUR4_mA.js} +1 -1
  68. package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-Dh3uJwRU.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-BLGvRDOX.js} +1 -1
  69. package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-BaIw2Adh.js → journeyDiagram-5HDEW3XC-c5xIah9o-D9zqo5k7.js} +1 -1
  70. package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-BLIQlGsE.js → kanban-definition-HUTT4EX6-Cnt6loYD-BwfjKxOx.js} +1 -1
  71. package/dist/assets/lib-BWq7d2p0.js +9 -0
  72. package/dist/assets/{line-D7ziSjKi-BnFPtJcd.js → line-D7ziSjKi-ZOy-21z8.js} +1 -1
  73. package/dist/assets/{marp-JKciXjDr.js → marp-IdFBx6Kk.js} +37 -37
  74. package/dist/assets/{mermaid-parser.core-DxEa8E3F-CscC8Qqr.js → mermaid-parser.core-DxEa8E3F-C_pDG6kL.js} +2 -2
  75. package/dist/assets/{mermaid.core-V0OYwIz3-g362i9RA.js → mermaid.core-V0OYwIz3--tUcNILl.js} +3 -3
  76. package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-CEmgqxM2.js → mindmap-definition-LN4V7U3C-CexN3O6L-B08uU8xg.js} +1 -1
  77. package/dist/assets/packet-7NZHBO7P-CQI3flND-C1R5Sk6K.js +1 -0
  78. package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-xR2ciIPB.js → pegDiagram-2B236MQR-BiM4G0cW-CAmtHOOs.js} +1 -1
  79. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-fnUCv-1K.js +1 -0
  80. package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-Do2rjuXg.js → pieDiagram-ENE6RG2P-B_fBBS-2-1Te1k2OW.js} +1 -1
  81. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CYjBsLx8.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-Bwmyhd3u.js} +1 -1
  82. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-DfxsmsH_.js +1 -0
  83. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-Cc9H2ZqM.js +1 -0
  84. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-CedxYTf_.js +1 -0
  85. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-DFUbcN03.js +1 -0
  86. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-7OkZDahF.js +1 -0
  87. package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-DrRDvGFv.js → railroadDiagram-RFXS5EU6--lNIlG64-B7viF_TQ.js} +1 -1
  88. package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-AohyoXS1.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-C5VA5j5p.js} +1 -1
  89. package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-BeuhDKAh.js → sankeyDiagram-HTMAVEWB-DlutaXDv-DfD-musw.js} +1 -1
  90. package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-BMS6Xghs.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-DeYKXJK9.js} +1 -1
  91. package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-1ccT8EBF.js → stateDiagram-2N3HPSRC-jf_dXUEU-Bq6BSHXK.js} +1 -1
  92. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-cQTUIwVP.js +1 -0
  93. package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-B7FjNmUf.js → swimlanes-5IMT3BWC-CYjtALQH-BwKcwh8j.js} +1 -1
  94. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-SzBmSlrT.js +8 -0
  95. package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-Cv5APELs.js → timeline-definition-FHXFAJF6-DYJ4oUm8-D5NlLypk.js} +1 -1
  96. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-mArgqXY_.js +1 -0
  97. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DcjdNdM5.js +1 -0
  98. package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-DDVhQ5s2.js → vennDiagram-L72KCM5P-CdKAHoek-BAq00OzF.js} +1 -1
  99. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-C9OSQ_B-.js +1 -0
  100. package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-BGrTblgg.js → wardleyDiagram-EHGQE667-BF6c4_CW-Dvh8PYFx.js} +1 -1
  101. package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-D9-hceiL.js → xychartDiagram-FW5EYKEG-CGiKngj7-C2sC6_Lu.js} +1 -1
  102. package/dist/index.html +3 -2
  103. package/package.json +15 -4
  104. package/server/agents/appended-prompt.ts +28 -0
  105. package/server/agents/claude-args.ts +11 -12
  106. package/server/agents/claude.ts +1 -0
  107. package/server/agents/codex-args.ts +30 -5
  108. package/server/agents/codex-rate-limits.spec.ts +20 -0
  109. package/server/agents/codex-rate-limits.ts +16 -6
  110. package/server/agents/codex-resume.ts +1 -1
  111. package/server/agents/codex-rollout.spec.ts +46 -0
  112. package/server/agents/codex-rollout.ts +13 -23
  113. package/server/agents/codex.ts +1 -0
  114. package/server/agents/probe-session.spec.ts +18 -0
  115. package/server/agents/probe-session.ts +10 -2
  116. package/server/agents/probe-transcript.spec.ts +282 -0
  117. package/server/agents/probe-transcript.ts +190 -0
  118. package/server/agents/rate-limit-persist.spec.ts +89 -0
  119. package/server/agents/rate-limit-persist.ts +24 -10
  120. package/server/agents/rate-limit-probe.spec.ts +52 -0
  121. package/server/agents/rate-limit-probe.ts +6 -2
  122. package/server/agents/rate-limit-store.spec.ts +15 -0
  123. package/server/agents/rate-limit-store.ts +8 -2
  124. package/server/agents/statusline.spec.ts +12 -3
  125. package/server/agents/statusline.ts +8 -7
  126. package/server/agents/types.ts +3 -0
  127. package/server/backends/collections.ts +15 -3
  128. package/server/backends/feed-summary.ts +1 -1
  129. package/server/backends/fileChange.ts +19 -6
  130. package/server/backends/fileOps.ts +5 -1
  131. package/server/backends/html.ts +67 -9
  132. package/server/backends/imageResult.ts +2 -2
  133. package/server/backends/markdown.ts +20 -9
  134. package/server/backends/mulmoscript.spec.ts +3 -3
  135. package/server/backends/openPath.ts +67 -0
  136. package/server/backends/remoteHost/googleCalendar.ts +9 -4
  137. package/server/backends/remoteHost/handlers.ts +2 -1
  138. package/server/backends/remoteHost/index.ts +18 -5
  139. package/server/backends/remoteHost/sessionActivity.ts +27 -3
  140. package/server/backends/remoteHost/terminalInput.ts +1 -1
  141. package/server/backends/remoteHost/terminalScreen.ts +26 -7
  142. package/server/backends/thumbnailStore.ts +5 -2
  143. package/server/cli-google.ts +1 -1
  144. package/server/config/app-config.ts +16 -0
  145. package/server/config/config-routes.ts +7 -0
  146. package/server/config/config-schema.ts +8 -0
  147. package/server/config/dir-config.ts +10 -2
  148. package/server/config/header-title.ts +7 -1
  149. package/server/files/pathContainment.ts +5 -1
  150. package/server/git/worktree-pr.ts +3 -3
  151. package/server/git/worktree-routes.ts +1 -1
  152. package/server/index.ts +127 -20
  153. package/server/infra/collection-tool.ts +1 -1
  154. package/server/infra/gui-mcp-registration.ts +90 -11
  155. package/server/infra/has-binary.ts +162 -14
  156. package/server/infra/jsonl-file.ts +88 -0
  157. package/server/infra/plugins-registry.ts +10 -1
  158. package/server/infra/resolve-bin.ts +3 -3
  159. package/server/infra/spawn-cwd.ts +46 -0
  160. package/server/infra/spawnCapture.ts +1 -1
  161. package/server/infra/tmux.ts +41 -0
  162. package/server/infra/web-push.ts +1 -1
  163. package/server/routes/app-routes.ts +24 -7
  164. package/server/routes/drop-routes.ts +50 -0
  165. package/server/routes/gui-mcp-routes.ts +5 -3
  166. package/server/routes/hook-routes.ts +26 -9
  167. package/server/routes/mcp-routes.ts +12 -2
  168. package/server/routes/plugin-routes.ts +11 -5
  169. package/server/routes/session-routes.ts +47 -2
  170. package/server/routes/ws-routes.ts +116 -39
  171. package/server/session/activity-transition.ts +9 -4
  172. package/server/session/cleared-transcripts.ts +118 -0
  173. package/server/session/completion-hooks.ts +51 -0
  174. package/server/session/cost.ts +23 -10
  175. package/server/session/decision-scan.ts +2 -10
  176. package/server/session/early-frames.ts +39 -0
  177. package/server/session/hook-settings.ts +1 -1
  178. package/server/session/last-turn.ts +6 -1
  179. package/server/session/launch-choice.ts +2 -2
  180. package/server/session/launcher-gui-mcp.ts +55 -0
  181. package/server/session/lifecycle.ts +17 -1
  182. package/server/session/mcp-config.ts +46 -2
  183. package/server/session/provider-env.ts +2 -2
  184. package/server/session/pty-connection.ts +12 -3
  185. package/server/session/pty-exit-log.ts +92 -0
  186. package/server/session/pty-spawn.ts +114 -10
  187. package/server/session/registry.ts +144 -37
  188. package/server/session/session-detail-view.ts +28 -6
  189. package/server/session/session-drops.ts +174 -0
  190. package/server/session/{dev-terminal-sessions.ts → session-id-log.ts} +16 -15
  191. package/server/session/session-list.ts +15 -4
  192. package/server/session/session-memos.ts +72 -0
  193. package/server/session/session-reads.ts +66 -54
  194. package/server/session/session-settings.ts +27 -4
  195. package/server/session/session-title.ts +25 -9
  196. package/server/session/session-tool-groups.ts +2 -2
  197. package/server/session/sessionListTitle.ts +13 -4
  198. package/server/session/spawn-claude.ts +59 -20
  199. package/server/session/spawn-codex.ts +31 -12
  200. package/server/session/spawn-shell.ts +10 -5
  201. package/server/session/summary-scan.ts +93 -0
  202. package/server/session/task-push.ts +10 -4
  203. package/server/session/taskPushRules.ts +5 -4
  204. package/server/session/terminal-replay.ts +15 -0
  205. package/server/session/tool-hook.ts +3 -3
  206. package/server/session/tool-store.ts +17 -19
  207. package/server/session/transcript.ts +35 -22
  208. package/server/session/types.ts +5 -3
  209. package/server/skills/mulmoterminal-bug-report/faq.md +6 -0
  210. package/server/skills/mulmoterminal-config/SKILL.md +34 -0
  211. package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-F7APysNp.js +0 -1
  212. package/dist/assets/channel-7wUqSdoX-o7hjueM_.js +0 -1
  213. package/dist/assets/chunk-FWX5IMBZ-DHLSFw1H-BtzzUMmo.js +0 -2
  214. package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-dAb2Nqej.js +0 -1
  215. package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-dAb2Nqej.js +0 -1
  216. package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-CcNtA1jr.js +0 -1
  217. package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-CWY7xW6B.js +0 -1
  218. package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-BQbNqK8I.js +0 -1
  219. package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DndYDaiz.js +0 -1
  220. package/dist/assets/index-BLn_JWl1.js +0 -620
  221. package/dist/assets/index-BZb3UNXf.css +0 -1
  222. package/dist/assets/info-DKCQHKI2-Dplx5kMp-Ch8HtmdN.js +0 -1
  223. package/dist/assets/lib-BkHCzFsd.js +0 -9
  224. package/dist/assets/packet-7NZHBO7P-CQI3flND-kVLgbrf9.js +0 -1
  225. package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-D1l75ZW4.js +0 -1
  226. package/dist/assets/radar-I7S5WNFK-us6x-Z9R-CgFu134o.js +0 -1
  227. package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-_koCxt7R.js +0 -1
  228. package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-y8_SdxMu.js +0 -1
  229. package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-CREWWR2J.js +0 -1
  230. package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DkxX3rOi.js +0 -1
  231. package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-COjV9W1k.js +0 -1
  232. package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CEARqU8w.js +0 -8
  233. package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-CrGq3OoF.js +0 -1
  234. package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-C7DIaiQ8.js +0 -1
  235. package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-WP0WCRL-.js +0 -1
  236. 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 (works where the browser exposes the path via
90
- `file://` Firefox/Safari), or click the **📎 file button** in the terminal header, which
91
- asks the local server to open the OS file dialog and inserts the chosen path (works in every
92
- browser, including Chrome). The path is inserted at the cursor — it is not submitted, so you
93
- can review it first.
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`; there is no setting to turn it off.
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. a live **AI title** the server generated for the session this run (see below),
1423
- 2. else the latest `ai-title` record's `aiTitle` (e.g. written by MulmoClaude),
1424
- 3. else the latest `last-prompt` record's `lastPrompt`,
1425
- 4. else the first real user message (slash/local-command wrappers like
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
- 5. else `"(untitled session)"`.
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;
@@ -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
+ }
@@ -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
- log("Setup done. Start MulmoTerminal: npx mulmoterminal");
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);
@@ -9,5 +9,7 @@ import type { SessionAgent } from "./sessionAgent.js";
9
9
  export interface QuickCommand {
10
10
  label: string;
11
11
  text: string;
12
- agents?: SessionAgent[];
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
  }
@@ -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
+ }