mulmoterminal 0.6.2 → 0.8.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 (166) hide show
  1. package/Dockerfile.sandbox +30 -0
  2. package/README.md +402 -38
  3. package/dist/assets/{abnfDiagram-VRR7QNED-6nNByj6v-BR__eiyl.js → abnfDiagram-VRR7QNED-6nNByj6v-Ce1WbMGC.js} +1 -1
  4. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-CkMBxcjf.js +1 -0
  5. package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bgnyaj_i-qvSM6uDf.js → architectureDiagram-ZJ3FMSHR-Bgnyaj_i-l_QglMuj.js} +1 -1
  6. package/dist/assets/{blockDiagram-677ZJIJ3-DQ35o5E4-Dkdpl09F.js → blockDiagram-677ZJIJ3-DQ35o5E4-B7PBZdY-.js} +1 -1
  7. package/dist/assets/{c4Diagram-LMCZKHZV-ClWZeiWo-DG-F0pLX.js → c4Diagram-LMCZKHZV-ClWZeiWo-Ddp2t63A.js} +1 -1
  8. package/dist/assets/channel-Di5rtkx0-CYQQVi2N.js +1 -0
  9. package/dist/assets/{chunk-32BRIVSS-CCt9wtYd-DL8ZqSXh.js → chunk-32BRIVSS-CCt9wtYd-Zqys0yUx.js} +1 -1
  10. package/dist/assets/{chunk-52WLFC77-FbBbR4uI-FcUCvUWm.js → chunk-52WLFC77-FbBbR4uI-DUyWsQQG.js} +1 -1
  11. package/dist/assets/{chunk-C7G6YPKG-C87hlS9c-BglxD8Ds.js → chunk-C7G6YPKG-C87hlS9c-Cq_SmMH6.js} +1 -1
  12. package/dist/assets/{chunk-EX3LRPZG-BqGqMXLN-BPw6bHPB.js → chunk-EX3LRPZG-BqGqMXLN-Sph0jyY5.js} +1 -1
  13. package/dist/assets/{chunk-FWX5IMBZ-BkbSAAuW-BeKan3Ew.js → chunk-FWX5IMBZ-BkbSAAuW-BlO-Dd6B.js} +2 -2
  14. package/dist/assets/{chunk-HOUHSVGY-Cxu0eDlh-BKGIWayl.js → chunk-HOUHSVGY-Cxu0eDlh-CB1-bkOQ.js} +1 -1
  15. package/dist/assets/{chunk-ICXQ74PX-hiraF_Xj-DqOS9Mmk.js → chunk-ICXQ74PX-hiraF_Xj-DL_g6Dk_.js} +1 -1
  16. package/dist/assets/{chunk-MOJQB5TN-CqxshQHA-o-9QjKwX.js → chunk-MOJQB5TN-CqxshQHA-C1do_qle.js} +1 -1
  17. package/dist/assets/{chunk-OGEWGWER-CjCr7ceX-kayseL-o.js → chunk-OGEWGWER-CjCr7ceX-Bphm1jnw.js} +1 -1
  18. package/dist/assets/{chunk-PUDLZKDR-Dx6M-vz1-CP23uMqS.js → chunk-PUDLZKDR-Dx6M-vz1-BqWQHrIk.js} +1 -1
  19. package/dist/assets/{chunk-Q4XR5HBZ-CZd-9lTB-DlNEhGNE.js → chunk-Q4XR5HBZ-CZd-9lTB-DCx-gEjT.js} +1 -1
  20. package/dist/assets/{chunk-V7JOEXUC-rI0xlC_O-CU1r_K83.js → chunk-V7JOEXUC-rI0xlC_O-C5XJW9gM.js} +1 -1
  21. package/dist/assets/{chunk-VAUOI2AC-DrcykVNK-Bx9_R8iV.js → chunk-VAUOI2AC-DrcykVNK-DNVpp2Bo.js} +1 -1
  22. package/dist/assets/{chunk-VR4S4FIN-O6iF8Yvf-Bxf47BHX.js → chunk-VR4S4FIN-O6iF8Yvf-uu4sPk_E.js} +1 -1
  23. package/dist/assets/{chunk-WYO6CB5R-B83L_z6I-DmI80qCd.js → chunk-WYO6CB5R-B83L_z6I-D-ILEUuT.js} +1 -1
  24. package/dist/assets/{chunk-ZGVPDNZ5-BpFv9JSP-Dtt7_IXx.js → chunk-ZGVPDNZ5-BpFv9JSP-B1byH7Ri.js} +1 -1
  25. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-Bzx8Q5qy.js +1 -0
  26. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-Bzx8Q5qy.js +1 -0
  27. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-CGx24Ele.js +1 -0
  28. package/dist/assets/{cynefinDiagram-TSTJHNR4-x0-0sQ15-FQw_dzEQ.js → cynefinDiagram-TSTJHNR4-x0-0sQ15-D_ZPdHMW.js} +1 -1
  29. package/dist/assets/{dagre-VKFMJZFB-CQdfl-bx-C8T5lvuy.js → dagre-VKFMJZFB-CQdfl-bx-B1T3S0FV.js} +1 -1
  30. package/dist/assets/{diagram-FQU43EPY-BOSB6VUb-BpatmYur.js → diagram-FQU43EPY-BOSB6VUb-4dUBHv_w.js} +1 -1
  31. package/dist/assets/{diagram-G47NLZAW-DLXrcXsN-BxnfDEMb.js → diagram-G47NLZAW-DLXrcXsN-B7k1DThA.js} +1 -1
  32. package/dist/assets/{diagram-NH7WQ7WH-BMQp1rkF-CWqxxOxM.js → diagram-NH7WQ7WH-BMQp1rkF-Pgg190OT.js} +1 -1
  33. package/dist/assets/{diagram-OA4YK3LP-D1wQ0vUj-DQ4TKrkb.js → diagram-OA4YK3LP-D1wQ0vUj-D9NWXoRR.js} +1 -1
  34. package/dist/assets/{diagram-WEI45ONY-RR0DpF8R-BdFnjQKM.js → diagram-WEI45ONY-RR0DpF8R-B6gyFifz.js} +1 -1
  35. package/dist/assets/{ebnfDiagram-CCIWWBDH-M123uVJ8-B5xFis-E.js → ebnfDiagram-CCIWWBDH-M123uVJ8-Bq7bwZP7.js} +1 -1
  36. package/dist/assets/{erDiagram-Q63AITRT-BWx_-PXG-b7pLAdQs.js → erDiagram-Q63AITRT-BWx_-PXG-BBuIT9tu.js} +1 -1
  37. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-C9YZ30Wv.js +1 -0
  38. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-DhOuIXQo.js +1 -0
  39. package/dist/assets/{ganttDiagram-NO4QXBWP-BOoJ1eTw-DJM3yEgW.js → ganttDiagram-NO4QXBWP-BOoJ1eTw-S7LYEVQU.js} +1 -1
  40. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-CAvO-lxi.js +1 -0
  41. package/dist/assets/{gitGraphDiagram-IHSO6WYX-B2CJhk_G-DvqS3rw6.js → gitGraphDiagram-IHSO6WYX-B2CJhk_G-BrDr5LgG.js} +1 -1
  42. package/dist/assets/index-B2YpCMoV.css +1 -0
  43. package/dist/assets/index-kwYCy5ON.js +357 -0
  44. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-DvahfbDn.js +1 -0
  45. package/dist/assets/{infoDiagram-FWYZ7A6U-Mp1X3pBP-BH22go43.js → infoDiagram-FWYZ7A6U-Mp1X3pBP-l542FG7h.js} +1 -1
  46. package/dist/assets/{ishikawaDiagram-FXEZZL3T-BNG7tkJu-DXVJne9S.js → ishikawaDiagram-FXEZZL3T-BNG7tkJu-CcYmE3rZ.js} +1 -1
  47. package/dist/assets/{journeyDiagram-5HDEW3XC-Dbp_hY9X-DCaJQ65f.js → journeyDiagram-5HDEW3XC-Dbp_hY9X-8sbAzdt7.js} +1 -1
  48. package/dist/assets/{kanban-definition-HUTT4EX6-DSTc5u3q-BvDaV1wR.js → kanban-definition-HUTT4EX6-DSTc5u3q-B4fcq4Yd.js} +1 -1
  49. package/dist/assets/{line-B1wBwzrY-BjL1-kEt.js → line-B1wBwzrY-BioBhEqP.js} +1 -1
  50. package/dist/assets/marp-B82QTNqJ.js +3458 -0
  51. package/dist/assets/{mermaid-parser.core-DC7NPJ_M-ylD2dn6D.js → mermaid-parser.core-DC7NPJ_M-YbqcHI8U.js} +2 -2
  52. package/dist/assets/{mermaid.core-DZM3Ha-E-BqB2NKvm.js → mermaid.core-DZM3Ha-E-2xRmGsJ0.js} +3 -3
  53. package/dist/assets/{mindmap-definition-LN4V7U3C-DYtgcMsY-D30h8tu0.js → mindmap-definition-LN4V7U3C-DYtgcMsY-BZWR0bBa.js} +1 -1
  54. package/dist/assets/packet-7NZHBO7P-lwb58iYx-Buaj8ce2.js +1 -0
  55. package/dist/assets/{pegDiagram-2B236MQR-C43eIpKM-DCkvtTme.js → pegDiagram-2B236MQR-C43eIpKM-CQpl0W1R.js} +1 -1
  56. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-Cb9jy6Ak.js +1 -0
  57. package/dist/assets/{pieDiagram-ENE6RG2P-BkTqgJyR-C9ImPAXx.js → pieDiagram-ENE6RG2P-BkTqgJyR-D2pqsEbd.js} +1 -1
  58. package/dist/assets/{quadrantDiagram-ABIIQ3AL-Bm1Zjm45-BzatICF4.js → quadrantDiagram-ABIIQ3AL-Bm1Zjm45-DYSLjuyi.js} +1 -1
  59. package/dist/assets/radar-I7S5WNFK-7CKcb_l--CsSL-fa_.js +1 -0
  60. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-wK2xnWS3.js +1 -0
  61. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--Ceaeq1Pr.js +1 -0
  62. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-fogZbeWf.js +1 -0
  63. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-mkHR05ng.js +1 -0
  64. package/dist/assets/{railroadDiagram-RFXS5EU6-BkfbdeAs-Bh9xEuq9.js → railroadDiagram-RFXS5EU6-BkfbdeAs-CeM-YRhH.js} +1 -1
  65. package/dist/assets/{requirementDiagram-TGXJPOKE-CrGTTjYg-BQgmdVSv.js → requirementDiagram-TGXJPOKE-CrGTTjYg-5vdLxYpa.js} +1 -1
  66. package/dist/assets/{sankeyDiagram-HTMAVEWB-rWXPf03Z-DscRUZXT.js → sankeyDiagram-HTMAVEWB-rWXPf03Z-JPVcutnp.js} +1 -1
  67. package/dist/assets/{sequenceDiagram-DBY2YBRQ-nkJYWO2m-lgs6nNUU.js → sequenceDiagram-DBY2YBRQ-nkJYWO2m-BxcX08nA.js} +1 -1
  68. package/dist/assets/{stateDiagram-2N3HPSRC-T4-clK8b-DNeEa7rO.js → stateDiagram-2N3HPSRC-T4-clK8b-DihpQxVg.js} +1 -1
  69. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-DtpIS_aL.js +1 -0
  70. package/dist/assets/{swimlanes-5IMT3BWC-HmQNEntu--YSC6ziW.js → swimlanes-5IMT3BWC-HmQNEntu-D3ChOi5U.js} +1 -1
  71. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-DdAPDSsw.js +8 -0
  72. package/dist/assets/{timeline-definition-FHXFAJF6-CNc9jSTP-C7YxL8qv.js → timeline-definition-FHXFAJF6-CNc9jSTP-BdGwxVIL.js} +1 -1
  73. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-BGcDmSH1.js +1 -0
  74. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-Clh2VUDB.js +1 -0
  75. package/dist/assets/{vennDiagram-L72KCM5P-Dr3pTJ_0-BnYpqsms.js → vennDiagram-L72KCM5P-Dr3pTJ_0-xTmIjEaL.js} +1 -1
  76. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-BnqfBQz5.js +1 -0
  77. package/dist/assets/{wardleyDiagram-EHGQE667-DSFc7ZZa-CEBr6tXd.js → wardleyDiagram-EHGQE667-DSFc7ZZa-C03D4kJr.js} +1 -1
  78. package/dist/assets/{xychartDiagram-FW5EYKEG-BP0Nn4Pp-BiY7UtJ1.js → xychartDiagram-FW5EYKEG-BP0Nn4Pp-DsSSHDBJ.js} +1 -1
  79. package/dist/index.html +2 -2
  80. package/package.json +10 -6
  81. package/server/agents/claude.ts +10 -0
  82. package/server/agents/codex.ts +7 -0
  83. package/server/agents/registry.spec.ts +44 -0
  84. package/server/agents/registry.ts +13 -0
  85. package/server/agents/types.ts +11 -0
  86. package/server/app-config.spec.ts +47 -9
  87. package/server/app-config.ts +54 -2
  88. package/server/backends/collections.spec.ts +107 -2
  89. package/server/backends/collections.ts +540 -388
  90. package/server/backends/remoteHost/attachmentStore.spec.ts +34 -0
  91. package/server/backends/remoteHost/attachmentStore.ts +61 -0
  92. package/server/backends/remoteHost/collectionPage.ts +25 -0
  93. package/server/backends/remoteHost/firebase.ts +28 -0
  94. package/server/backends/remoteHost/getFeed.spec.ts +94 -0
  95. package/server/backends/remoteHost/handlers.spec.ts +132 -0
  96. package/server/backends/remoteHost/handlers.ts +202 -0
  97. package/server/backends/remoteHost/index.ts +98 -0
  98. package/server/backends/remoteHost/ingestAttachments.spec.ts +91 -0
  99. package/server/backends/remoteHost/ingestAttachments.ts +98 -0
  100. package/server/backends/remoteHost/onExpire.ts +51 -0
  101. package/server/backends/remoteHost/skills.spec.ts +82 -0
  102. package/server/backends/remoteHost/skills.ts +90 -0
  103. package/server/backends/remoteView.ts +296 -0
  104. package/server/backends/shortcuts.ts +3 -2
  105. package/server/backends/thumbnailStore.spec.ts +61 -0
  106. package/server/backends/thumbnailStore.ts +104 -0
  107. package/server/backends/workspaceSetup.ts +8 -0
  108. package/server/claude-args.spec.ts +7 -3
  109. package/server/claude-args.ts +6 -9
  110. package/server/codex-args.spec.ts +41 -0
  111. package/server/codex-args.ts +30 -0
  112. package/server/codex-session.spec.ts +142 -0
  113. package/server/codex-session.ts +121 -0
  114. package/server/codex-sessions.spec.ts +96 -0
  115. package/server/codex-sessions.ts +140 -0
  116. package/server/codex-skills.spec.ts +70 -0
  117. package/server/codex-skills.ts +54 -0
  118. package/server/command-summary.spec.ts +122 -0
  119. package/server/command-summary.ts +155 -0
  120. package/server/config-routes.ts +60 -19
  121. package/server/cost.spec.ts +107 -0
  122. package/server/cost.ts +215 -0
  123. package/server/dir-config.spec.ts +63 -4
  124. package/server/dir-config.ts +47 -3
  125. package/server/files-browse.ts +25 -22
  126. package/server/git-status.spec.ts +101 -0
  127. package/server/git-status.ts +54 -0
  128. package/server/header-config.spec.ts +108 -0
  129. package/server/header-config.ts +189 -0
  130. package/server/header-context.spec.ts +16 -0
  131. package/server/header-context.ts +68 -0
  132. package/server/header-resolve.spec.ts +132 -0
  133. package/server/header-resolve.ts +125 -0
  134. package/server/index.ts +577 -145
  135. package/server/sandbox.spec.ts +172 -0
  136. package/server/sandbox.ts +271 -0
  137. package/server/terminal-replay.spec.ts +41 -0
  138. package/server/terminal-replay.ts +24 -0
  139. package/server/tmux.spec.ts +17 -1
  140. package/server/tmux.ts +40 -4
  141. package/server/transcript.spec.ts +88 -0
  142. package/server/transcript.ts +66 -0
  143. package/dist/assets/architecture-TIHT7OUA-CYMWc3UT-DYR5YD-t.js +0 -1
  144. package/dist/assets/channel-Di5rtkx0-CFIdTKCo.js +0 -1
  145. package/dist/assets/classDiagram-OUVF2IWQ-BgAZMSbT-BJvb6DM5.js +0 -1
  146. package/dist/assets/classDiagram-v2-EOCWNBFH-DxHTyui1-BJvb6DM5.js +0 -1
  147. package/dist/assets/cynefin-VYW2F7L2-DqA3n9nY-Co1DLFQX.js +0 -1
  148. package/dist/assets/eventmodeling-45OFAUF4-_BVSjAXf-3OIl1Ld2.js +0 -1
  149. package/dist/assets/flowDiagram-23GEKE2U-BeOc_anm-DTd1kyw3.js +0 -1
  150. package/dist/assets/gitGraph-TEB2WS4Q-CH12KLTN-BASrfYk-.js +0 -1
  151. package/dist/assets/index-BycNzdB5.js +0 -355
  152. package/dist/assets/index-Dj98e4nG.css +0 -1
  153. package/dist/assets/info-DKCQHKI2-Cbw3mbiK-QQNWL0Ad.js +0 -1
  154. package/dist/assets/marp-BlNCU8cR.js +0 -3451
  155. package/dist/assets/packet-7NZHBO7P-lwb58iYx-CphYqnZh.js +0 -1
  156. package/dist/assets/pie-RZYD4A2V-B1UWb4Gu-D0ZU1Leh.js +0 -1
  157. package/dist/assets/radar-I7S5WNFK-7CKcb_l--DtPkSnh9.js +0 -1
  158. package/dist/assets/railroad-3IZDKUUU-gCySKdnW-Du5FA7n1.js +0 -1
  159. package/dist/assets/railroad-abnf-AHOZXSZD-BophH4r--DFxlg3Lr.js +0 -1
  160. package/dist/assets/railroad-ebnf-EBAXGLYW-AZNjl_Zu-CGHvjWhk.js +0 -1
  161. package/dist/assets/railroad-peg-LSFZ7HO6-BSiEEyeb-CFiLH0rH.js +0 -1
  162. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIp7nhRd-B0W-KHx7.js +0 -1
  163. package/dist/assets/swimlanesDiagram-G3AALYLV-BeoZhwg7-Bpt8MBzh.js +0 -8
  164. package/dist/assets/treeView-QDETBFTQ-nIQcG1h9-N1od_Hb2.js +0 -1
  165. package/dist/assets/treemap-6X3UGDF4-BnsvC8yL-BWBbeqvg.js +0 -1
  166. package/dist/assets/wardley-OPB4EBWU-8Odxkx6V-DvUPwP_X.js +0 -1
@@ -0,0 +1,30 @@
1
+ # MulmoTerminal sandbox image: runs the interactive `claude` CLI inside a container so
2
+ # an untrusted workspace can't touch the host. Deliberately minimal — unlike MulmoClaude
3
+ # this image needs NO in-container MCP broker, because MulmoTerminal's GUI MCP is served
4
+ # over HTTP by the host and reached at host.docker.internal (see docs/spawn-architecture).
5
+ #
6
+ # What's inside: node (for the claude CLI), curl (the activity hooks POST to the host),
7
+ # git + ripgrep + ca-certificates (claude's built-in tools). No secrets, no host FS.
8
+ FROM node:22-slim
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ git \
12
+ curl \
13
+ ca-certificates \
14
+ ripgrep \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # The interactive agent CLI. Pin nothing here — a rebuild picks up the latest, matching
18
+ # how the host runs the newest `claude`.
19
+ RUN npm install -g @anthropic-ai/claude-code
20
+
21
+ # Run as the unprivileged `node` user (uid 1000). Phase 1 is macOS-only: Docker Desktop
22
+ # maps bind-mount ownership transparently there. The server gates the sandbox off on Linux
23
+ # (no `--user` yet → uid-1000 ownership mismatch) and Windows (host paths aren't valid
24
+ # Linux container paths), falling back to the host spawn — see Phase 5 (#202).
25
+ USER node
26
+ ENV HOME=/home/node
27
+ WORKDIR /home/node/workspace
28
+
29
+ # The host passes `claude ...` (with a rewritten --mcp-config / --settings) as the
30
+ # command, so no ENTRYPOINT/CMD baked in — the container IS one claude session.
package/README.md CHANGED
@@ -1,14 +1,21 @@
1
1
  # mulmoterminal
2
2
 
3
- A browser-based terminal for running [Claude Code](https://claude.com/claude-code)
4
- sessions, with a sidebar that lists the current project's chat sessions and shows
5
- live, hook-driven activity for each one.
3
+ A browser-based terminal + GUI workspace for coding agents — [Claude Code](https://claude.com/claude-code)
4
+ and OpenAI's **Codex** — with a sidebar that lists the current project's chat sessions
5
+ and shows live, hook-driven activity for each one.
6
6
 
7
- Each session runs as a real PTY on the server (`claude` in a pseudo-terminal) and
7
+ Each session runs as a real PTY on the server (the agent CLI in a pseudo-terminal) and
8
8
  is streamed to an [xterm.js](https://xtermjs.org/) terminal in the browser over a
9
- WebSocket. A sidebar lists every Claude session for the project and reflects, in
10
- real time, which sessions are **working** (Claude is thinking) and which **need
11
- attention** (waiting for input, or finished with output you haven't seen).
9
+ WebSocket. A sidebar lists every session for the project and reflects, in real time,
10
+ which sessions are **working** (the agent is thinking) and which **need attention**
11
+ (waiting for input, or finished with output you haven't seen).
12
+
13
+ Around the terminal sit the things a coding session needs: a **grid** of parallel
14
+ sessions, an optional **GUI panel** for rich tool output (documents, forms, images,
15
+ charts), **git worktree** isolation with one-click **push / open PR**, a full-screen
16
+ **file browser + editor**, per-session **cost & token** readouts, a cross-repo **PRs &
17
+ Issues** view, and read-only **Wiki** and **Collections** browsers over the shared
18
+ workspace.
12
19
 
13
20
  **Inserting a file path** — like a native terminal, you can put a file's absolute
14
21
  path into the prompt: **drag a file** onto the terminal (works where the browser
@@ -22,7 +29,10 @@ inserted at the cursor — it is not submitted, so you can review it first.
22
29
  ## Install & run
23
30
 
24
31
  Requires the [`claude`](https://claude.com/claude-code) CLI on your `PATH` and
25
- **Node ≥ 22.9**.
32
+ **Node ≥ 22.9**. Optional but recommended: **`tmux`** so terminals survive a server
33
+ restart (see [Session persistence (tmux)](#session-persistence-tmux)), the **`gh`** CLI
34
+ logged in for the PRs/Issues view and one-click PR creation, and — for Codex sessions —
35
+ the **`codex`** CLI on your `PATH`.
26
36
 
27
37
  ```bash
28
38
  npx mulmoterminal # start on http://localhost:34567 and open the browser
@@ -54,15 +64,26 @@ server, and opens the browser. For local development from a clone, see
54
64
 
55
65
  - [Architecture](#architecture)
56
66
  - [Why a PTY?](#why-a-pty)
67
+ - [Agents: Claude & Codex](#agents-claude--codex)
68
+ - [Session persistence (tmux)](#session-persistence-tmux)
69
+ - [Docker sandbox (experimental, single view)](#docker-sandbox-experimental-single-view)
57
70
  - [Tech stack](#tech-stack)
58
71
  - [Configuration](#configuration)
59
72
  - [Running](#running)
60
73
  - [Scripts (Run menu)](#scripts-run-menu)
74
+ - [Files view (browse & edit)](#files-view-browse--edit)
75
+ - [Git worktrees & pull requests](#git-worktrees--pull-requests)
76
+ - [Cost & token usage](#cost--token-usage)
77
+ - [Wiki, Collections & the GUI panel](#wiki-collections--the-gui-panel)
78
+ - [More features](#more-features)
61
79
  - [Server API specification](#server-api-specification)
62
80
  - [HTTP: `GET /api/sessions`](#http-get-apisessions)
63
81
  - [HTTP: `GET /api/scripts`](#http-get-apiscripts)
82
+ - [HTTP: `POST /api/command/summarize`](#http-post-apicommandsummarize)
64
83
  - [HTTP: `POST /api/hook`](#http-post-apihook)
84
+ - [More HTTP endpoints](#more-http-endpoints)
65
85
  - [WebSocket: `/ws` (terminal)](#websocket-ws-terminal)
86
+ - [More WebSocket endpoints](#more-websocket-endpoints)
66
87
  - [WebSocket: `/ws/run` (command terminal)](#websocket-wsrun-command-terminal)
67
88
  - [Socket.IO: `/ws/pubsub` (activity pub/sub)](#socketio-wspubsub-activity-pubsub)
68
89
  - [Session model](#session-model)
@@ -93,14 +114,15 @@ server, and opens the browser. For local development from a clone, see
93
114
  - **Session list** is fetched over HTTP (`/api/sessions`).
94
115
  - **Live activity** is pushed over a Socket.IO pub/sub channel (`/ws/pubsub`);
95
116
  the server learns of activity from **Claude hooks** that POST to `/api/hook`.
96
- - **Script commands** (`yarn dev`, tests, …), launched from a cell's directory
97
- picker, run in their own ephemeral PTY over a separate WebSocket (`/ws/run`);
98
- they are not Claude sessions. See [Scripts (Run menu)](#scripts-run-menu).
117
+ - **Other terminals** run on their own raw WebSockets: **Codex** sessions on `/ws/codex`,
118
+ persistent **launch commands** on `/ws/launch`, and one-off **script commands**
119
+ (`yarn dev`, tests, …) on `/ws/run`. Only Claude/Codex are agent sessions with hooks;
120
+ see [Agents: Claude & Codex](#agents-claude--codex) and [Scripts (Run menu)](#scripts-run-menu).
99
121
  - In dev (`yarn dev`) the Vite dev server runs on its own port (`CLIENT_PORT`,
100
- default `6856`) and proxies `/ws` (covers `/ws/run`), `/ws/pubsub`, and `/api` to
101
- the backend (`PORT`, default `34567`) so you open the Vite port (e.g.
102
- `http://localhost:6856`). In production the backend serves the built client from
103
- `dist/` on `PORT`, and you open that.
122
+ default `6856`) and proxies `/ws` (a prefix covering `/ws/codex`, `/ws/launch`, and
123
+ `/ws/run`), `/ws/pubsub`, `/api`, and `/artifacts` to the backend (`PORT`, default
124
+ `34567`) — so you open the Vite port (e.g. `http://localhost:6856`). In production the
125
+ backend serves the built client from `dist/` on `PORT`, and you open that.
104
126
 
105
127
  ---
106
128
 
@@ -121,6 +143,38 @@ SDK; we drive the real interactive CLI and relay its TTY over the WebSocket.
121
143
 
122
144
  ---
123
145
 
146
+ ## Agents: Claude & Codex
147
+
148
+ MulmoTerminal drives **interactive coding-agent CLIs**, not just Claude. An
149
+ `AgentAdapter` seam abstracts the per-agent bits (which binary to spawn, how it resumes)
150
+ so the PTY, grid, persistence, and GUI-panel plumbing stay shared. Two adapters ship
151
+ today — **Claude Code** (the default) and **Codex**.
152
+
153
+ - **Claude** — spawned as `claude` (override with `CLAUDE_BIN`). The server passes
154
+ `--session-id <uuid>`, so it knows the live session's id even before its transcript
155
+ file exists, and injects activity hooks + the GUI MCP per spawn (see
156
+ [Claude hook injection](#claude-hook-injection)).
157
+ - **Codex** — spawned as `codex` (override with `CODEX_BIN`; `CODEX_MODEL` sets
158
+ `--model`). Codex runs on its own WebSocket (`/ws/codex`) and its sessions appear in the
159
+ sidebar next to Claude's. Because Codex only mints its rollout id **after** the first
160
+ turn, the server watches `~/.codex/sessions/**/rollout-*.jsonl` (home overridable via
161
+ `CODEX_HOME`) and maps the new rollout to the session — attributed only when it's
162
+ unambiguous, never by "newest wins". Resume reattaches a live PTY, adopts a surviving
163
+ tmux session, or cold-resumes the rollout id.
164
+
165
+ **Choosing an agent.** The single view has a **New Codex session** button; each grid
166
+ cell's launch form and the Collections browser carry a **Claude / Codex** toggle (your
167
+ choice is remembered).
168
+
169
+ **Skills for Codex.** Codex has no `/<slug>` slash commands, so on session setup
170
+ MulmoTerminal **mirrors the workspace's `.claude/skills` into `~/.codex/skills`** (each
171
+ mirrored directory carries a `.mt-mirror` marker so a re-sync overwrites what MulmoTerminal
172
+ owns and never clobbers Codex's own skills), and rewrites a collection's `/<slug> …` seed
173
+ into a plain `Use the "<slug>" skill.` instruction. The same skills Claude uses then show
174
+ up for Codex, loaded by description.
175
+
176
+ ---
177
+
124
178
  ## Session persistence (tmux)
125
179
 
126
180
  If **`tmux` is installed**, MulmoTerminal runs each Claude session and launcher inside
@@ -134,14 +188,68 @@ it never touches your personal tmux sessions or keybindings.
134
188
  before. An explicit close (a cell's ✕) ends the tmux session; a machine reboot does not
135
189
  survive (tmux itself is gone). Command-cell scripts are ephemeral and not persisted.
136
190
 
191
+ **Installing tmux** (optional):
192
+
193
+ ```bash
194
+ brew install tmux # macOS (Homebrew)
195
+ sudo apt install tmux # Debian / Ubuntu
196
+ sudo dnf install tmux # Fedora
197
+ ```
198
+
199
+ On Windows there's no native tmux, so sessions use the non-persistent fallback — run the
200
+ server under **WSL** if you want persistence. Nothing else is required: MulmoTerminal
201
+ detects `tmux` on `PATH` at startup and uses it automatically when present.
202
+
203
+ ---
204
+
205
+ ## Docker sandbox (experimental, single view)
206
+
207
+ Set **`MULMOTERMINAL_SANDBOX=1`** (and have Docker running) to run the **single-view**
208
+ Claude session inside a container instead of on the host, while Claude still reaches the
209
+ app's GUI MCP + activity hooks over `host.docker.internal`. The `mulmoterminal-sandbox`
210
+ image is **built automatically** on first launch from the shipped `Dockerfile.sandbox`
211
+ (~1 min, once; rebuilt only when that file changes). Override the name with
212
+ `MULMOTERMINAL_SANDBOX_IMAGE`. If the image can't be built (e.g. Docker down), the session
213
+ falls back to the host spawn — no cryptic failure.
214
+
215
+ This **contains** Claude — it can't reach the host filesystem outside the mounts, host
216
+ processes, or arbitrary host ports. It is **not full isolation**: the **workspace** and
217
+ **`~/.claude`** are bind-mounted **read-write** by design (so Claude edits your project,
218
+ and transcripts interoperate with host sessions), so those specific paths stay mutable
219
+ from inside. The sandbox is **non-persistent** (the container is
220
+ `--rm`, tied to the session), **opt-in and single-view only** — the grid keeps its host +
221
+ tmux path, and with the flag unset (or Docker unavailable) everything runs on the host
222
+ exactly as before. **macOS only** for now — on Linux (bind-mount uid ownership) and
223
+ Windows (host paths aren't valid Linux container paths) it falls back to the host spawn;
224
+ both are follow-ups. Adding arbitrary user MCP servers to the sandbox is in progress
225
+ (see #202).
226
+
227
+ **Authentication (macOS).** Claude's live login token lives in the macOS **Keychain**,
228
+ which the container can't read (mounting `~/.claude` alone isn't enough — its
229
+ `.credentials.json` is often absent or stale). On each sandbox spawn MulmoTerminal exports
230
+ the current credential to a per-session `~/.mulmoterminal/sandbox/creds-<id>.json`
231
+ (mode `0600`, removed when the session ends) and mounts it **read-only** over the
232
+ container's `~/.claude/.credentials.json`; your host `~/.claude` is never modified. If
233
+ you've never logged in on the host, run `claude` once first — otherwise the server logs a
234
+ warning and the container shows "Not logged in".
235
+
236
+ **Host credentials (opt-in).** By default the sandbox has no host credentials. To let the
237
+ sandboxed Claude use `gh`/`git`, set **`SANDBOX_MOUNT_CONFIGS=gh,gitconfig`** — a **fixed
238
+ allowlist** (you pick names, never arbitrary paths): `gh` mounts `~/.config/gh` read-only
239
+ and passes a `GH_TOKEN` (from `gh auth token`, since macOS keeps it in the Keychain), and
240
+ `gitconfig` mounts `~/.gitconfig` read-only. Set **`SANDBOX_SSH_AGENT_FORWARD=1`** to
241
+ forward the SSH agent socket (the keys never enter the container). Both are read only when
242
+ building the sandbox spawn, so they have no effect unless `MULMOTERMINAL_SANDBOX` is on.
243
+
137
244
  ---
138
245
 
139
246
  ## Tech stack
140
247
 
141
248
  | Layer | Technology |
142
249
  | -------- | ---------- |
143
- | Frontend | Vue 3 (`<script setup>` + TypeScript), Vite, xterm.js (`@xterm/*`), socket.io-client |
144
- | Backend | Node (ESM, TypeScript run via `tsx`), Express 5, `ws` (terminal WebSocket), `node-pty`, socket.io |
250
+ | Frontend | Vue 3 (`<script setup>` + TypeScript), Vue Router, Vite, xterm.js (`@xterm/*`), CodeMirror 6, socket.io-client |
251
+ | Backend | Node (ESM, TypeScript run via `tsx`), Express 5, `ws` (terminal WebSocket), `node-pty`, socket.io, `@modelcontextprotocol/sdk` (in-process GUI MCP) |
252
+ | Plugins | GUI-protocol Vue plugins (`@mulmoclaude/*`, `@mulmochat-plugin/*`): markdown, form, image, chart, HTML, collection, accounting |
145
253
  | Tests | Vitest + @vue/test-utils + jsdom |
146
254
 
147
255
  Requires **Node ≥ 22.9** (uses `node --env-file-if-exists`) and the `claude` CLI on `PATH`.
@@ -161,6 +269,17 @@ the server runs without one.
161
269
  | `CLIENT_PORT` | `6856` | Vite dev-server port (dev only: the URL you open with `yarn dev`). |
162
270
  | `CLAUDE_BIN` | `claude` | The Claude Code binary to spawn. |
163
271
  | `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). |
272
+ | `CLAUDE_PERMISSION_MODE` | `auto` | Permission mode passed to each `claude` spawn. |
273
+ | `CODEX_BIN` | `codex` | The Codex CLI binary to spawn. |
274
+ | `CODEX_MODEL`| codex default | Model passed to Codex as `--model` (unset = Codex's own default). |
275
+ | `CODEX_HOME` | `~/.codex` | Codex home — where its session rollouts and MulmoTerminal-mirrored skills live. |
276
+ | `MULMOTERMINAL_HOME` | `~/.mulmoterminal` | Root for managed **git worktrees**. |
277
+ | `WAIT_REAP_GRACE_MS` | `1800000` | How long a **waiting** background session is kept before it's auto-reaped (`0` or negative = never). |
278
+
279
+ The Docker-sandbox variables (`MULMOTERMINAL_SANDBOX`, `MULMOTERMINAL_SANDBOX_IMAGE`,
280
+ `SANDBOX_MOUNT_CONFIGS`, `SANDBOX_SSH_AGENT_FORWARD`) and the update-check opt-outs
281
+ (`MULMOTERMINAL_NO_UPDATE_CHECK`, `NO_UPDATE_NOTIFIER`) are covered in
282
+ [Docker sandbox](#docker-sandbox-experimental-single-view) and [Install & run](#install--run).
164
283
 
165
284
  Example `.env` (gitignored):
166
285
 
@@ -179,6 +298,7 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
179
298
  | `soundFile` | Absolute path to a custom **attention sound** (played when a session needs attention). Empty/unset uses the built-in synthesized chime. |
180
299
  | `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates (via your `gh` login). |
181
300
  | `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides Claude — a plain shell, `codex`, any interactive command. |
301
+ | `userMcpServers` | `{ id, url }` HTTP MCP servers merged into the **single-view** Claude session's `--mcp-config` (a `localhost` URL is reached over `host.docker.internal` in the Docker sandbox). Takes effect on the next session. |
182
302
 
183
303
  **Attention sound.** The default chime is generated with the Web Audio API — **no
184
304
  audio file is bundled**, so the npm package stays light and has no media-licensing
@@ -199,6 +319,12 @@ malformed file is ignored.
199
319
  {
200
320
  "name": "PROD · payments", // badge shown on this directory's terminals
201
321
  "badgeColor": "#cf222e", // badge color (hex #rrggbb)
322
+ "headerColor": "#190a23", // cell header background (hex #rrggbb)
323
+ "headerTextColor": "#ffffff", // cell header text color (hex #rrggbb)
324
+ "cellColor": "#101014", // cell body background (hex #rrggbb)
325
+ "cellBorderColor": "#2a2a4e", // cell border color (hex #rrggbb)
326
+ "dotColor": "#00e676", // idle status dot (hex #rrggbb)
327
+ "buttonColor": "#c7cdf0", // header icon buttons (hex #rrggbb)
202
328
  "theme": "nord", // terminal palette: midnight | nord | daylight | solarized
203
329
  "colors": { "background": "#190a23", "cursor": "#ff2e63" }, // per-key palette overrides
204
330
  "sound": "./.mulmoterminal/alert.mp3" // attention sound, RELATIVE to this directory
@@ -209,6 +335,12 @@ malformed file is ignored.
209
335
  | ------------ | ------- |
210
336
  | `name` | Label shown as a badge in the terminal/cell header. |
211
337
  | `badgeColor` | Badge background color (`#rrggbb`); text auto-contrasts. |
338
+ | `headerColor` | Header **background** color (`#rrggbb`) — the grid cell's header row and the terminal's own header row (grid row 2 + single view). While a terminal is working/blocked the status tint still shows; the custom color applies when idle. |
339
+ | `headerTextColor` | Header **text** color (`#rrggbb`) — the dir path, title, and prompt. |
340
+ | `cellColor` | Cell **body background** color (`#rrggbb`) — the frame around the terminal. |
341
+ | `cellBorderColor` | Cell **border** color (`#rrggbb`). The status frame (working/blocked) still overrides it while active. |
342
+ | `dotColor` | **Idle** status-dot color (`#rrggbb`). The working/waiting colors are unchanged so the activity signal stays intact. |
343
+ | `buttonColor` | Header **icon button** color (`#rrggbb`) — expand / close / attach / folder / etc., across both header rows. |
212
344
  | `theme` | xterm palette for terminals in this directory (one of the built-in theme ids). |
213
345
  | `colors` | Per-key xterm palette overrides applied on top of `theme` (or the app theme when `theme` is unset). Keys are xterm `ITheme` names (`background`, `foreground`, `cursor`, `selectionBackground`, the 16 ANSI colors, …); values are hex (`#rgb` / `#rrggbb` / `#rrggbbaa`). Unknown keys / bad values are dropped. |
214
346
  | `sound` | Attention sound for this directory's sessions, a path **relative to the directory** (served at `GET /api/dir-sound`). |
@@ -300,6 +432,13 @@ offers a **↻ re-run**. The browser only ever sends the script's **index** + it
300
432
  directory; the server reads that directory's `script.json` and resolves the
301
433
  command, so the file is the allowlist of what can run.
302
434
 
435
+ Each command cell also has a **✦ Summarize** button: click it to send the cell's
436
+ captured output to `claude -p` (headless) and get a short **Errors / Warnings /
437
+ likely cause / suggested fix** note in a panel — handy when a build or install
438
+ buries the one failing line in thousands. It's manual (never auto-runs) and analyzes
439
+ the last 32 KB of output. See
440
+ [`POST /api/command/summarize`](#http-post-apicommandsummarize).
441
+
303
442
  ---
304
443
 
305
444
  ## Files view (browse & edit)
@@ -318,6 +457,119 @@ the terminal is pointed at.
318
457
 
319
458
  ---
320
459
 
460
+ ## Git worktrees & pull requests
461
+
462
+ When a terminal's directory is a git repo, its header shows a **branch chip**
463
+ (`⎇ <branch>` with dirty / ahead / behind counts), fed by `GET /api/git-status` (polled
464
+ while the view is visible). A **GitHub** menu links straight to the repo, its issues, and
465
+ its pull requests.
466
+
467
+ **Worktree isolation.** A grid cell's launch form offers **+ New worktree**: name a task
468
+ and the cell launches its agent inside a fresh
469
+ [git worktree](https://git-scm.com/docs/git-worktree) on a new `agent/<slug>` branch — a
470
+ separate working tree that shares the repo's `.git`, so several agents can work the same
471
+ repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
472
+ `MULMOTERMINAL_HOME`), and existing ones are listed for reuse.
473
+
474
+ A worktree cell's header carries a **diff badge** (`+<commits> ●<dirty>`); click it for a
475
+ **Changes vs `<base>`** panel (file list + patch) with actions:
476
+
477
+ - **✓ Commit** — hands the cell's own session a canned commit prompt.
478
+ - **⬆ Push** — `git push -u origin <branch>` (`POST /api/worktrees/push`).
479
+ - **⧉ Open PR** — pushes, then `gh pr create … --fill`; if `gh` is missing or unauthed it
480
+ falls back to opening the GitHub **compare** URL (`POST /api/worktrees/pr`).
481
+
482
+ Closing a worktree cell asks whether to **keep** the worktree or **discard & remove** it
483
+ (a dirty worktree is never removed unless you confirm).
484
+
485
+ **PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
486
+ view that aggregates open PRs **and** issues across the repos listed in Settings →
487
+ **Pull request repos** (`prRepos`, `owner/repo` entries) via your server-side `gh` login.
488
+ PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
489
+ issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
490
+ independently. Backed by `GET /api/prs` and `GET /api/issues`.
491
+
492
+ ---
493
+
494
+ ## Cost & token usage
495
+
496
+ Each grid cell's header shows two badges for its session, refreshed when a turn finishes
497
+ (from `GET /api/session/:id`):
498
+
499
+ - **Context badge** — e.g. `Opus · ctx 35%`: the model family plus how full its context
500
+ window is (the *last* turn's input + cache tokens ÷ the model's window — **1M** for
501
+ current-gen Opus / Sonnet / Fable, **200k** otherwise; an unknown model shows the label
502
+ with no %).
503
+ - **Token badge** — `⇡<in> ⇣<out>`: cumulative input (fresh + cache-read + cache-creation)
504
+ and output tokens for the session, k/M-formatted, with a full breakdown in the tooltip.
505
+
506
+ The **Settings** modal (⚙) shows an **estimated $ cost** — Session / Today / Month — from
507
+ `GET /api/cost`, using a built-in public per-model price table (cache reads billed at
508
+ 0.1×, cache writes at 1.25× input). It's an estimate: real billing differs, **flat-plan
509
+ (Max) usage isn't reflected**, and turns on unpriced models are flagged and excluded.
510
+
511
+ A separate, full **double-entry accounting** book (the `account_balance` toolbar button →
512
+ `/accounting`) is provided by the bundled `@mulmoclaude/accounting-plugin` and stores its
513
+ books under `<workspace>/data/accounting`. It's a bookkeeping app — unrelated to the LLM
514
+ cost estimate above — and is also exposed to Claude as the `manageAccounting` GUI tool.
515
+
516
+ ---
517
+
518
+ ## Wiki, Collections & the GUI panel
519
+
520
+ MulmoTerminal is also a **live view over the shared workspace** (`CLAUDE_CWD`, default
521
+ `~/mulmoclaude`) that agents author into — never a snapshot, so it re-reads on entry.
522
+
523
+ **GUI panel.** Beside the terminal, a **GUI panel** ("Canvas") renders the rich results of
524
+ GUI-protocol tools the agent calls — documents (`presentDocument`), forms (`presentForm`),
525
+ generated images, charts, HTML, and collection cards. Each result is drawn by its plugin's
526
+ own Vue view inside a Shadow-DOM `PluginFrame` (so a plugin's bundled CSS can't leak),
527
+ mirrors the active session, and replays history on re-select. Plugins reach the agent over
528
+ an **in-process MCP server** served per session at `POST /api/mcp/:sessionId` (server name
529
+ `mulmoterminal-gui`) — which works from the host *or* the Docker sandbox (over
530
+ `host.docker.internal`). Which plugins load is gated by `plugins/plugins.json`; the shipped
531
+ set includes markdown, form, image generation (needs `GEMINI_API_KEY`), chart, HTML, and
532
+ collection views. You can also merge your **own HTTP MCP servers** into the single-view
533
+ session via Settings → `userMcpServers`.
534
+
535
+ **Wiki.** The toolbar **Wiki** button opens a read-only browser over `<workspace>/data/wiki/`
536
+ — an **index** (tag-filterable page catalog), rendered **pages** with `[[wiki links]]` and
537
+ backlinks, a **graph** view (pages ranked by references), and a **lint** report (orphans /
538
+ broken links / tag drift). Read-only endpoints: `GET /api/wiki`, `/api/wiki/graph`,
539
+ `/api/wiki/lint`.
540
+
541
+ **Collections.** The toolbar **Collections** button browses the workspace's collection
542
+ "cards" (`@mulmoclaude/collection-plugin`). Running a collection **action** fetches a seed
543
+ prompt and spawns a fresh agent session for it — the **Launch with Claude / Codex** toggle
544
+ decides which agent (and whether the seed auto-runs or drops in as an editable draft).
545
+ Favorited collections get their own toolbar buttons.
546
+
547
+ ---
548
+
549
+ ## More features
550
+
551
+ - **Grid of parallel sessions** — the + Terminal / grid view runs many sessions at once,
552
+ auto-sizing by count across pages. Cell borders signal state at a glance — **working**
553
+ (pulsing blue), **blocked** (amber — needs a permission / answer), **done** (blue —
554
+ finished, output unreviewed), and **idle** — and the toolbar shows a tally across all
555
+ pages so you notice an off-screen cell that needs you.
556
+ - **Timeline** (🕘) — a read-only per-session activity timeline (tools run, newest first),
557
+ from `GET /api/transcript/timeline`.
558
+ - **Tools pane** — the available GUI tools plus a live tool-call history for the active
559
+ session.
560
+ - **Notifications** (🔔) — a toolbar bell with an unread badge and a dropdown of active
561
+ notifications; click a row to jump to its session.
562
+ - **Voice input** — dictate a prompt via on-device Whisper (`POST /api/transcribe`, macOS
563
+ only; the model downloads on first use).
564
+ - **Remote host** — link MulmoTerminal to the companion phone client (Google sign-in) to
565
+ watch and start sessions from your phone.
566
+ - **Themes** — four terminal palettes (midnight / nord / daylight / solarized), your pick
567
+ remembered; a project's `.mulmoterminal.json` can override per directory.
568
+ - **Editing niceties** — **Shift+Enter** inserts a newline in the prompt, and on macOS
569
+ **Option** is treated as Meta so Claude's Alt-key bindings work.
570
+
571
+ ---
572
+
321
573
  ## Server API specification
322
574
 
323
575
  Base URL: `http://localhost:$PORT` (default `http://localhost:34567`).
@@ -376,6 +628,33 @@ position the client sends back to `/ws/run`).
376
628
  A missing or invalid `script.json` is **not** an error — it yields an empty
377
629
  `scripts` array.
378
630
 
631
+ ### HTTP: `POST /api/command/summarize`
632
+
633
+ Runs `claude -p` **headless** over a command cell's captured terminal output and
634
+ returns a short summary (Errors / Warnings / likely cause / suggested fix). Backs the
635
+ **✦ Summarize** button on a Run cell (see [Scripts (Run menu)](#scripts-run-menu)).
636
+ The browser sends the cell's xterm buffer as `log`; the server truncates it to the
637
+ last **32 KB** (the tail, where errors + the exit line live), runs the CLI with the
638
+ log piped on stdin (argv — no shell), and returns its answer. Same-origin guarded.
639
+
640
+ **Request `application/json`**:
641
+
642
+ ```jsonc
643
+ { "log": "npm ERR! cannot find module 'foo'\n..." }
644
+ ```
645
+
646
+ **Response `200 application/json`**:
647
+
648
+ ```jsonc
649
+ {
650
+ "summary": "Errors: cannot find module 'foo'\nSuggested fix: run `yarn add foo`",
651
+ "truncated": false // true when the log exceeded 32 KB and only the tail was analyzed
652
+ }
653
+ ```
654
+
655
+ Empty output returns a `{ summary }` note rather than calling the CLI. Errors:
656
+ `400` (missing `log`), `403` (disallowed origin), `502` (the `claude` run failed).
657
+
379
658
  ### HTTP: `POST /api/hook`
380
659
 
381
660
  **Internal endpoint.** Claude hooks (injected per session — see
@@ -403,6 +682,62 @@ Any resulting state change is published on the `sessions` pub/sub channel.
403
682
 
404
683
  **Response `200 application/json`**: `{ "ok": true }` (always, even for unknown events).
405
684
 
685
+ ### More HTTP endpoints
686
+
687
+ The endpoints above are the core; the server exposes many more (all under
688
+ `http://localhost:$PORT`; query params shown where relevant). Mutating endpoints are
689
+ same-origin-guarded.
690
+
691
+ **Sessions & agents**
692
+
693
+ | Endpoint | Purpose |
694
+ | -------- | ------- |
695
+ | `GET /api/session/:id?cwd=` | One session's summary — cumulative `usage` and `context` (model + last-turn context tokens). Backs the cell token & ctx% badges. |
696
+ | `GET /api/codex/sessions?cwd=` | Codex sessions for the project (from `~/.codex` rollouts), newest first. |
697
+ | `GET /api/cost?cwd=&session=` | Estimated $ cost — session / today / month. |
698
+ | `GET /api/transcript/timeline?session=&cwd=` | Per-session activity timeline (tools run). |
699
+
700
+ **Git & worktrees**
701
+
702
+ | Endpoint | Purpose |
703
+ | -------- | ------- |
704
+ | `GET /api/git-status?cwd=` | `{ repo, branch, detached, dirty, ahead, behind, upstream }`. |
705
+ | `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
706
+ | `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
707
+ | `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>`, remove (managed root only), push, open a PR (`gh`, else compare URL). |
708
+ | `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` (via `gh`). |
709
+
710
+ **Workspace views**
711
+
712
+ | Endpoint | Purpose |
713
+ | -------- | ------- |
714
+ | `GET /api/wiki` (`?slug=`) · `/api/wiki/graph` · `/api/wiki/lint` | Read-only wiki index / page / graph / lint. |
715
+ | `GET /api/collections/…` · `/api/feeds` · `GET\|PUT /api/shortcuts` | Collections browser, feeds, favorites (see `docs/collection-plugin-integration.md`). |
716
+ | `GET /api/files/browse/{list,text,md}` · `PUT /api/files/browse/write` | File tree / read / Markdown-render / write (contained within the project root). |
717
+ | `GET /api/files/raw?path=` | Raw asset bytes (workspace-rooted). |
718
+
719
+ **GUI panel / plugins / MCP**
720
+
721
+ | Endpoint | Purpose |
722
+ | -------- | ------- |
723
+ | `POST /api/mcp/:sessionId` | Per-session GUI MCP server (Streamable HTTP; `GET`/`DELETE` → 405). |
724
+ | `POST /api/plugin/:toolName` | GUI-plugin dispatch (incl. `spawnBackgroundChat`, `manageAccounting`, `presentHtml`). |
725
+ | `GET /api/agent/toolResults/:id` · `POST /api/agent/toolResult` | GUI-panel result history / persist. |
726
+ | `GET /api/tools` · `GET /api/tool-calls/:id` | Available tools / tool-call history. |
727
+ | `POST /api/accounting` | Double-entry accounting (bundled plugin). |
728
+
729
+ **Config, sound & misc**
730
+
731
+ | Endpoint | Purpose |
732
+ | -------- | ------- |
733
+ | `GET\|POST /api/config` | User UI config (`cwdPresets`, `soundFile`, `prRepos`, `launchers`, `userMcpServers`). |
734
+ | `GET /api/sound` · `/api/dir-sound?cwd=` · `/api/dir-config?cwd=` | Custom / per-directory attention sound + per-dir config. |
735
+ | `GET /api/notifications`(`/history`) · `POST /api/notifications/:id/clear` | Notification feed. |
736
+ | `POST /api/transcribe`(`/model`…) | Voice-input transcription (Whisper, macOS). |
737
+ | `POST /api/translation` | Runtime UI-string translation. |
738
+ | `GET /api/remote-host/status` · `POST /api/remote-host/{connect,disconnect}` | Companion phone-client link. |
739
+ | `POST /api/open-dir` · `POST /api/pick-file` | Reveal a dir in Finder/Explorer; OS file-picker → path. |
740
+
406
741
  ### WebSocket: `/ws` (terminal)
407
742
 
408
743
  A raw WebSocket carrying the terminal stream for one session. One PTY per
@@ -438,6 +773,20 @@ A non-JSON frame is written to the PTY verbatim (fallback).
438
773
  **kept alive** in the background; otherwise it's killed. See
439
774
  [Session lifecycle](#session-lifecycle).
440
775
 
776
+ ### More WebSocket endpoints
777
+
778
+ Two more raw WebSockets share the `/ws` frame format (`output` / `input` / `resize` /
779
+ `exit`):
780
+
781
+ - **`/ws/codex?session=<id>&cwd=<dir>&gui=<0|1>`** — a **Codex** agent PTY (see
782
+ [Agents: Claude & Codex](#agents-claude--codex)). Like `/ws` it sends a `session` frame
783
+ with the id and reattaches to a live or tmux-backed session on resume. `gui=0` (grid
784
+ cells) omits the GUI MCP and keeps the session out of the sidebar.
785
+ - **`/ws/launch?session=<id>&cwd=<dir>&launcher=<index>`** — a **launch command** PTY (a
786
+ plain shell, `codex`, or any command configured in Settings → Launch commands). Unlike a
787
+ Run-menu script it's **persistent and reattachable** (survives page switches /
788
+ reconnects), but it has no Claude hooks, so its dot only shows running vs. exited.
789
+
441
790
  ### WebSocket: `/ws/run` (command terminal)
442
791
 
443
792
  A raw WebSocket carrying a one-off **Run-menu command** (see
@@ -603,29 +952,44 @@ appears, at which point the on-disk title takes over.
603
952
 
604
953
  ```
605
954
  server/
606
- index.js Express app, /api routes, terminal WebSocket, PTY lifecycle,
607
- session state, hook injection, session discovery; also
608
- /api/scripts + the /ws/run command-terminal relay
609
- scripts.ts Loads/validates script.json; resolves a Run-menu command by index
610
- pubsub.js createPubSub(server) socket.io pub/sub at /ws/pubsub
611
- fix-pty-perms.js postinstall: fixes node-pty prebuilt binary permissions
955
+ index.ts Express app, /api routes, upgrade routing, PTY lifecycle,
956
+ session state, hook injection, session discovery, GUI-MCP mount
957
+ agents/ AgentAdapter seam claude.ts, codex.ts, registry.ts
958
+ codex-*.ts Codex sessions, args, rollout discovery, skill mirroring
959
+ tmux.ts tmux-backed session persistence (own -L mulmoterminal server)
960
+ sandbox.ts Docker sandbox spawn for the single-view Claude session
961
+ worktrees.ts, worktree-*.ts git worktree create/list/diff/push/PR + routes
962
+ git-status.ts, gitRemote.ts, gh.ts, prs.ts, issues.ts git & GitHub (via gh)
963
+ cost.ts, transcript.ts cost estimate; usage/context from transcripts
964
+ files-browse.ts file tree + read/write (contained to project root)
965
+ scripts.ts Run-menu script.json loader
966
+ command-summary.ts POST /api/command/summarize (claude -p headless)
967
+ app-config.ts, config-routes.ts, dir-config.ts user + per-directory config
968
+ plugins-registry.ts, mcp/ GUI plugin registry + per-session MCP broker
969
+ backends/ wiki, collections, feeds, accounting, notifier,
970
+ translation, whisper, remote-host, html, files
971
+ pubsub.ts socket.io pub/sub at /ws/pubsub
972
+ fix-pty-perms.js postinstall: fixes node-pty binary permissions
612
973
  src/
613
- App.vue Layout (sidebar + terminal); owns the active session id
974
+ App.vue Layout; owns the active session + single/grid view
975
+ router/ Vue Router routes (/, /terminals, /collections,
976
+ /accounting, /prs, /files, /wiki, …)
614
977
  components/
615
- Sidebar.vue Session list; working dot + waiting bold; pub/sub driven
616
- Sidebar.spec.ts Vitest component tests
617
- Terminal.vue xterm.js terminal; /ws (or /ws/run); single-view ▶ Run menu
618
- AppToolbar.vue Shared header (single + grid); grid-only + Terminal + ⇅/↔ cell-order toggle
619
- GridView.vue Grid view: auto-layout, pages, manual/auto cell order; runs handed-off scripts
620
- RunMenu.vue Run dropdown: lists a dir's script.json, emits the pick
621
- TerminalCell.vue A cell: Claude launcher (dir picker + resume + run-a-script); ◀▶ to reorder
622
- TerminalGrid.vue Grid of cells; auto-sizes by count; zoom lines up every tab
623
- CommandCell.vue A grid cell that runs a script.json command (ephemeral)
624
- composables/
625
- usePubSub.ts socket.io-client pub/sub composable (subscribe/unsubscribe)
626
- usePendingScript.ts Hands a header-picked script to the grid to run
627
- useUnloadGuard.ts Confirm before closing/reloading the tab while a terminal is live
628
- vite.config.ts Dev proxy for /ws (covers /ws/run), /ws/pubsub, /api
978
+ Sidebar.vue, SessionTabBar.vue session list + tab bar (pub/sub driven)
979
+ Terminal.vue xterm.js terminal; /ws, /ws/codex, /ws/run
980
+ AppToolbar.vue shared header + toolbar buttons
981
+ GridView.vue, TerminalGrid.vue, TerminalCell.vue, CommandCell.vue, LauncherCell.vue
982
+ GuiPanel.vue, PluginFrame.vue GUI panel (Canvas) + Shadow-DOM plugin host
983
+ FilesOverlay.vue file browser + CodeMirror editor
984
+ GitBranchChip.vue, ModelContextBadge.vue header chips / badges
985
+ PrsOverlay.vue cross-repo PRs & Issues
986
+ Wiki*View.vue, Collections*.vue, AccountingOverlay.vue workspace views
987
+ TimelineOverlay.vue, ToolsPane.vue, NotificationBell.vue, RemoteHostControl.vue
988
+ SettingsModal.vue ⚙ settings
989
+ composables/ useSessions, usePubSub, useGitStatus, useCost,
990
+ useChatLauncher, useFilesView, useWikiBrowse,
991
+ useCollectionBrowse, useNotifications, useVoiceInput,
992
+ vite.config.ts Dev proxy for /ws (+ /ws/codex, /ws/launch, /ws/run), /ws/pubsub, /api, /artifacts
629
993
  vitest.config.ts jsdom test environment
630
994
  ```
631
995
 
@@ -1 +1 @@
1
- import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DC7NPJ_M-ylD2dn6D.js";import"./chunk-WYO6CB5R-B83L_z6I-DmI80qCd.js";import"./chunk-VAUOI2AC-DrcykVNK-Bx9_R8iV.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CqxshQHA-o-9QjKwX.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
1
+ import{n as e}from"./chunk-Y2CYZVJY-Bdt8pFDJ-DsF7k-Jl.js";import{f as t}from"./src-Brzfja-q-ch1VcTiG.js";import{_ as n,g as r}from"./mermaid-parser.core-DC7NPJ_M-YbqcHI8U.js";import"./chunk-WYO6CB5R-B83L_z6I-D-ILEUuT.js";import"./chunk-VAUOI2AC-DrcykVNK-DNVpp2Bo.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CqxshQHA-C1do_qle.js";import{t as s}from"./chunk-JWPE2WC7-BQ3zXr2k-DSd3Ct95.js";var c=r().RailroadAbnf.parser.LangiumParser,l=e(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=e(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=e(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=e(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=e(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=e(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=e(e=>{s(e,a),e.title&&a.setTitle(e.title),e.rules.map(e=>a.addRule(m(e)))},`populateDb`),g={parser:{parse:e(e=>{a.clear(),t.debug(`[ABNF Parser] Starting Langium parse`);let r=c.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new n(r);let i=r.value;t.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),t.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:a}},db:a,renderer:o,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import{f as e}from"./mermaid-parser.core-DC7NPJ_M-YbqcHI8U.js";export{e as createArchitectureServices};