mulmoterminal 4.1.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -25
- package/common/byCodeUnit.ts +11 -0
- package/common/codeBlocks.ts +9 -5
- package/common/gitlabHosts.ts +60 -0
- package/common/isRecord.ts +6 -0
- package/common/isUnknownArray.ts +8 -0
- package/common/issueStartPlan.ts +18 -11
- package/common/keymap.ts +2 -0
- package/common/median.ts +7 -2
- package/common/prPhase.ts +3 -3
- package/common/readString.ts +28 -0
- package/common/repoEntry.ts +3 -2
- package/common/toolGroups.ts +37 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-RjiYivmv-CuUAI5MR.js → abnfDiagram-VRR7QNED-RjiYivmv-Bdog-CiH.js} +1 -1
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-rsT_LNkQ.js +1 -0
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-3nWA91tG-rRDT09Vx.js → architectureDiagram-ZJ3FMSHR-3nWA91tG-DY8jZRJO.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-BPuAJQRW-By0PkzIO.js → blockDiagram-677ZJIJ3-BPuAJQRW-CpOQ2oxU.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-C0LAqQso-GgOhwf63.js → c4Diagram-LMCZKHZV-C0LAqQso-CWAcQCwl.js} +1 -1
- package/dist/assets/channel-7wUqSdoX-C_3NKRG1.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-BRrYpgtb-C_AxGcxt.js → chunk-32BRIVSS-BRrYpgtb-BzcHZdCO.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BJ-ss3Xr-DNopp04b.js → chunk-52WLFC77-BJ-ss3Xr-Qq7iypNS.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-BZEucKEL---6fNwPJ.js → chunk-C7G6YPKG-BZEucKEL-DU94jTaQ.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-DLS6FBN1-D5lN82UI.js → chunk-EX3LRPZG-DLS6FBN1-B8t9ONXm.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DHLSFw1H-DKxptXUj.js → chunk-FWX5IMBZ-DHLSFw1H-72qiJJFi.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-Bhlt8hXJ-CAh9s_YU.js → chunk-HOUHSVGY-Bhlt8hXJ-Dh_S4m7L.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-DwgHBX_g-jwQc10EC.js → chunk-ICXQ74PX-DwgHBX_g-i4M4RWjR.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CMZRaeqt-DcPt_trW.js → chunk-MOJQB5TN-CMZRaeqt-ePkETRCM.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-8Qy4a8b5-BuRF4rGR.js → chunk-OGEWGWER-8Qy4a8b5-BMPSaD1n.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-DcrWQRYh-BYgad2gt.js → chunk-PUDLZKDR-DcrWQRYh-CUfUNRZU.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-ZXVGkG8Z-DhnrB6RT.js → chunk-Q4XR5HBZ-ZXVGkG8Z-vRCdUFJt.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-DmGdheTX-Ur1KzthC.js → chunk-V7JOEXUC-DmGdheTX-BPzQ8JEc.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CS9QJ4yz-BZe0Q9XX.js → chunk-VAUOI2AC-CS9QJ4yz-DDJgABqC.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C6a91eNY-2iUlb5mQ.js → chunk-VR4S4FIN-C6a91eNY-DZS-oJom.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-BlzOfotS-CeZxJxL0.js → chunk-WYO6CB5R-BlzOfotS-DMj-dLn7.js} +1 -1
- package/dist/assets/{chunk-ZGVPDNZ5-BYwxNFTK-Ct0mEJmO.js → chunk-ZGVPDNZ5-BYwxNFTK-tmcZjNpX.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-I_77uR8C.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-I_77uR8C.js +1 -0
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-BASJNDQ6.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-DHA9iPo--BISNJm8X.js → cynefinDiagram-TSTJHNR4-DHA9iPo--BpiB89n2.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB--oJKqXBZ-B459NNxB.js → dagre-VKFMJZFB--oJKqXBZ-Ddms1XzR.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CcJCB9bG-DVyaiIsO.js → diagram-FQU43EPY-CcJCB9bG-BAHWVmAY.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-C_o-WGG1-BhuGPhW4.js → diagram-G47NLZAW-C_o-WGG1-DdaYFG9H.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CXJCYvY--DzbRZHC1.js → diagram-NH7WQ7WH-CXJCYvY--CHgaNPap.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BMzeJ87A-D8t4UJW_.js → diagram-OA4YK3LP-BMzeJ87A-C7QgoRtB.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-D_93NKqo-Bbl2-CHc.js → diagram-WEI45ONY-D_93NKqo-DK7fOC2g.js} +1 -1
- package/dist/assets/{dist-D8AEcEHx.js → dist-1yVb2v37.js} +1 -1
- package/dist/assets/{dist-Daghl_nN.js → dist-B8QpBddG.js} +1 -1
- package/dist/assets/{dist-epQ42VP2.js → dist-BvnEqyXN.js} +1 -1
- package/dist/assets/{dist-ClO6UEBJ.js → dist-N43w-1Uv.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVai1Ii9-Bbg4X-2o.js → ebnfDiagram-CCIWWBDH-CVai1Ii9-CVGfPNmr.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CiABnA0s-CKq_tipK.js → erDiagram-Q63AITRT-CiABnA0s-DY1yINPc.js} +1 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-DYbmMHSK.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-DxbRE59B.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-DQZvdFo1-jmx958Wo.js → ganttDiagram-NO4QXBWP-DQZvdFo1-BFu7XeTn.js} +1 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-DDSyOJ1L.js +1 -0
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-C2ovBouh-DuZaMN46.js → gitGraphDiagram-IHSO6WYX-C2ovBouh-CVNiO1pR.js} +1 -1
- package/dist/assets/index-C-UrTIoI.css +1 -0
- package/dist/assets/index-CHZiol4M.js +614 -0
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-bM6jK96q.js +1 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-7UnoB5AP-C8ikfN1o.js → infoDiagram-FWYZ7A6U-7UnoB5AP-C93AGObv.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-ByUDM_N2-ChrD-r9s.js → ishikawaDiagram-FXEZZL3T-ByUDM_N2-B2Dn2jAL.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-c5xIah9o-uY7L1btR.js → journeyDiagram-5HDEW3XC-c5xIah9o-DQk5r-7H.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-Cnt6loYD-CNv5ziZo.js → kanban-definition-HUTT4EX6-Cnt6loYD-1TZVG0gD.js} +1 -1
- package/dist/assets/{lib-dCbEkBG7.js → lib-JfWkHMZA.js} +1 -1
- package/dist/assets/{line-D7ziSjKi-BEYdEqj_.js → line-D7ziSjKi-DhbbwYBs.js} +1 -1
- package/dist/assets/{marp-CU_bIZEZ.js → marp-BfY_ww9b.js} +1 -1
- package/dist/assets/{mermaid-parser.core-DxEa8E3F-akMN_vOP.js → mermaid-parser.core-DxEa8E3F-D6oVwac4.js} +2 -2
- package/dist/assets/{mermaid.core-V0OYwIz3-BMPzsgXF.js → mermaid.core-V0OYwIz3-B6uMFLWJ.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CexN3O6L-Bed_YDhf.js → mindmap-definition-LN4V7U3C-CexN3O6L-B1FJ1HVs.js} +1 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-M2i4X2qk.js +1 -0
- package/dist/assets/{pegDiagram-2B236MQR-BiM4G0cW-CBrM86uu.js → pegDiagram-2B236MQR-BiM4G0cW--wr3I5uW.js} +1 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-Bd_XkMs5.js +1 -0
- package/dist/assets/{pieDiagram-ENE6RG2P-B_fBBS-2-D-tRTGSr.js → pieDiagram-ENE6RG2P-B_fBBS-2-_2feGczD.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-BigGVxCR-CH-H4qGn.js → quadrantDiagram-ABIIQ3AL-BigGVxCR-BrtQISoY.js} +1 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-C3vBieyU.js +1 -0
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-CGQcIWsR.js +1 -0
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-jWYezWwF.js +1 -0
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-Csc9w5bS.js +1 -0
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-D5bhRRBY.js +1 -0
- package/dist/assets/{railroadDiagram-RFXS5EU6--lNIlG64-jEmJgaFj.js → railroadDiagram-RFXS5EU6--lNIlG64-Cl00PZJ3.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-D-B0Y5Wu-DurumWyC.js → requirementDiagram-TGXJPOKE-D-B0Y5Wu-BBU6_YVY.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-DlutaXDv-Doed1fks.js → sankeyDiagram-HTMAVEWB-DlutaXDv-DDG1vVfm.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-BOLgtggH-CmkZuV9D.js → sequenceDiagram-DBY2YBRQ-BOLgtggH-BLRc-c61.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-jf_dXUEU-BFXZmQ_i.js → stateDiagram-2N3HPSRC-jf_dXUEU-IElGdQ_H.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-KNFxI9rB.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-CYjtALQH-e_82UQvE.js → swimlanes-5IMT3BWC-CYjtALQH-DWw64pD1.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-CjVzBocH.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-DYJ4oUm8-CO0rLezO.js → timeline-definition-FHXFAJF6-DYJ4oUm8-CmkjOn4R.js} +1 -1
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-Db61iIhG.js +1 -0
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-DX1IC1yH.js +1 -0
- package/dist/assets/{vennDiagram-L72KCM5P-CdKAHoek-BO9jvKIz.js → vennDiagram-L72KCM5P-CdKAHoek-Ch4eXyRT.js} +1 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-IFR8KKBh.js +1 -0
- package/dist/assets/{wardleyDiagram-EHGQE667-BF6c4_CW-eXxyipXb.js → wardleyDiagram-EHGQE667-BF6c4_CW-B2NgeiNl.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CGiKngj7-CMZLvO1e.js → xychartDiagram-FW5EYKEG-CGiKngj7-B544fj9E.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +15 -16
- package/server/agents/antigravity-mcp.ts +11 -4
- package/server/agents/antigravity-session.ts +1 -1
- package/server/agents/antigravity-sessions.ts +4 -2
- package/server/agents/codex-activity.ts +1 -1
- package/server/agents/codex-rate-limits.spec.ts +1 -0
- package/server/agents/codex-rate-limits.ts +1 -1
- package/server/agents/codex-rollout.spec.ts +1 -0
- package/server/agents/codex-session.ts +4 -2
- package/server/agents/codex-sessions.ts +3 -2
- package/server/agents/probe-session.spec.ts +1 -0
- package/server/agents/probe-stall.spec.ts +1 -0
- package/server/agents/probe-transcript.spec.ts +11 -1
- package/server/agents/rate-limit-persist.spec.ts +1 -0
- package/server/agents/rate-limit-probe.spec.ts +12 -1
- package/server/agents/rate-limit-store.spec.ts +1 -0
- package/server/agents/statusline.spec.ts +1 -0
- package/server/backends/collections.ts +5 -3
- package/server/backends/remoteHost/attachment-path.ts +1 -1
- package/server/backends/remoteHost/handlers/getCollection.ts +2 -1
- package/server/backends/remoteHost/handlers/getFeed.ts +2 -1
- package/server/backends/remoteHost/handlers/getRemoteView.ts +3 -2
- package/server/backends/remoteHost/handlers/getRemoteViewItems.ts +3 -2
- package/server/backends/remoteHost/handlers/issueWork.ts +6 -4
- package/server/backends/remoteHost/handlers/mutateRemoteView.ts +3 -2
- package/server/backends/remoteHost/issueWork.spec.ts +2 -1
- package/server/backends/remoteHost/routes.spec.ts +1 -0
- package/server/backends/remoteHost/session.spec.ts +1 -0
- package/server/backends/remoteHost/skills.ts +1 -1
- package/server/backends/remoteHost/terminalInput.ts +1 -1
- package/server/backends/remoteView.ts +2 -1
- package/server/backends/scheduler.ts +1 -0
- package/server/backends/translation.ts +7 -3
- package/server/backends/wiki.ts +3 -2
- package/server/config/app-config.ts +33 -4
- package/server/config/config-body.ts +1 -0
- package/server/config/config-routes.ts +16 -1
- package/server/config/dir-config.ts +1 -1
- package/server/config/header-title.ts +2 -2
- package/server/config/hue-rotate.ts +78 -0
- package/server/config/workspace.ts +2 -1
- package/server/config/worktree-dir-config.ts +74 -0
- package/server/files/backup-store.ts +5 -3
- package/server/files/files-browse.ts +5 -3
- package/server/files/pathContainment.ts +1 -1
- package/server/files/renderedDoc.ts +1 -0
- package/server/files/scripts.ts +1 -0
- package/server/git/forge-host.ts +18 -3
- package/server/git/forge-support.ts +8 -7
- package/server/git/git-parse.ts +2 -2
- package/server/git/gitRemote.ts +1 -1
- package/server/git/glab.ts +64 -12
- package/server/git/issue-work.ts +5 -3
- package/server/git/issues.ts +2 -2
- package/server/git/prPhase.ts +6 -6
- package/server/git/prs.ts +5 -4
- package/server/git/remote-ref.ts +1 -1
- package/server/git/work-comment.ts +7 -7
- package/server/git/worktree-routes.ts +6 -7
- package/server/git/worktrees.ts +26 -1
- package/server/index.ts +7 -3
- package/server/infra/accounting-tool.ts +2 -2
- package/server/infra/pluginRuntime.ts +1 -1
- package/server/infra/plugins-registry.ts +81 -29
- package/server/infra/process-guards.ts +5 -2
- package/server/infra/pty-env.ts +1 -0
- package/server/infra/pubsub.ts +2 -2
- package/server/infra/server-tool-load.ts +7 -2
- package/server/infra/tmux.ts +2 -2
- package/server/mcp/broker.ts +2 -2
- package/server/routes/gui-mcp-routes.ts +4 -2
- package/server/routes/hook-routes.ts +34 -24
- package/server/routes/issue-work-routes.ts +2 -1
- package/server/routes/mcp-routes.ts +2 -2
- package/server/routes/requestBody.ts +14 -0
- package/server/routes/session-routes.ts +14 -2
- package/server/routes/ws-routes.ts +39 -9
- package/server/session/cleared-transcripts.ts +1 -1
- package/server/session/command-summary.ts +1 -1
- package/server/session/decision-scan.ts +4 -1
- package/server/session/decisions.ts +1 -1
- package/server/session/last-turn.ts +6 -3
- package/server/session/launcher-gui-mcp.ts +45 -10
- package/server/session/mcp-config.ts +48 -4
- package/server/session/pty-connection.ts +17 -12
- package/server/session/registry.ts +10 -1
- package/server/session/scheduled-sessions.ts +1 -1
- package/server/session/screen-rows.ts +4 -2
- package/server/session/session-reads.ts +3 -2
- package/server/session/session-settings.ts +9 -0
- package/server/session/shell-command.ts +1 -1
- package/server/session/spawn-claude.ts +3 -18
- package/server/session/spawn-codex.ts +14 -8
- package/server/session/tmux-size-sync.ts +9 -3
- package/server/session/tool-hook.ts +6 -3
- package/server/session/tool-store.ts +28 -9
- package/server/session/transcript.ts +12 -7
- package/server/session/translation-worker.ts +8 -4
- package/server/skills/mulmoterminal-bug-report/faq.md +16 -0
- package/server/skills/mulmoterminal-config/SKILL.md +23 -1
- package/server/skills/mulmoterminal-dirs/SKILL.md +17 -0
- package/dist/assets/architecture-TIHT7OUA-0NsD2xW1-B89nYL-T.js +0 -1
- package/dist/assets/channel-7wUqSdoX-DLHsiYx_.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-BYlRi6YQ-vnBEnnpL.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-BCHaYHxj-vnBEnnpL.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CGYr2F6h-U6pbCywv.js +0 -1
- package/dist/assets/eventmodeling-45OFAUF4-CWmqhm3a-Dz8P7VVP.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DKSB5AiY-Bqi3MvHN.js +0 -1
- package/dist/assets/gitGraph-TEB2WS4Q-BR9qPwdN-C_bvrqXL.js +0 -1
- package/dist/assets/index-CXYjAh9r.css +0 -1
- package/dist/assets/index-D9sJGCR9.js +0 -616
- package/dist/assets/info-DKCQHKI2-Dplx5kMp-aO_hAsb7.js +0 -1
- package/dist/assets/packet-7NZHBO7P-CQI3flND-jjlc375I.js +0 -1
- package/dist/assets/pie-RZYD4A2V-CnmKE2Wa-BGKp68D7.js +0 -1
- package/dist/assets/radar-I7S5WNFK-us6x-Z9R-ChTyqNRH.js +0 -1
- package/dist/assets/railroad-3IZDKUUU-BKdz6pdN-BRepafjd.js +0 -1
- package/dist/assets/railroad-abnf-AHOZXSZD-m_CSSIfd-B2E_hGfU.js +0 -1
- package/dist/assets/railroad-ebnf-EBAXGLYW-Nu3nAG7C-C2k_5LtZ.js +0 -1
- package/dist/assets/railroad-peg-LSFZ7HO6--Y3mD4R_-DSwMdPCz.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CuzgCL2v-CZroC0CQ.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-3bmSBtKs-C2aatMMX.js +0 -8
- package/dist/assets/treeView-QDETBFTQ-D27HWG6Q-DhW5sK_F.js +0 -1
- package/dist/assets/treemap-6X3UGDF4-CmGIU3H5-oZY-xCg-.js +0 -1
- package/dist/assets/wardley-OPB4EBWU-CbHWsb2k-BCWqjXDb.js +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ coding with a single agent needs nothing but a shell — this is for when you ru
|
|
|
8
8
|
track of which is waiting. Sessions survive a reload (tmux), work isolates in **git worktrees**,
|
|
9
9
|
and a **phone push** reaches you when a turn finishes.
|
|
10
10
|
|
|
11
|
+
**Every cell is a real pty.** `htop`, `lazygit`, a dev server and Claude Code are the same kind
|
|
12
|
+
of object here — which is why the one-session-per-worktree limit applies to **agents only**, and
|
|
13
|
+
a shell or a `yarn dev` launcher can sit in the same worktree an agent is working in.
|
|
14
|
+
|
|
11
15
|
### 📖 Documentation — **[receptron.github.io/mulmoterminal](https://receptron.github.io/mulmoterminal/)**
|
|
12
16
|
|
|
13
17
|
- **User guide:** [English](https://receptron.github.io/mulmoterminal/guide/en/) — the grid
|
|
@@ -39,7 +43,7 @@ npx mulmoterminal@latest # starts on http://localhost:34567 and opens you
|
|
|
39
43
|
|
|
40
44
|

|
|
41
45
|
|
|
42
|
-
*The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working
|
|
46
|
+
*The grid is a **cockpit for parallel agents** — here, four live Claude sessions, each in its own color-coded project. Every cell's header carries what you need to triage at a glance: **model · context %**, **token counts** (`⇡in ⇣out`), the **git branch / changes** chip, and an AI summary of what the agent is doing. A cell's **border color signals state** — working (blue), done (green), needs-you (amber — e.g. waiting on a permission), idle — with an attention chime so a stuck cell off-screen still pulls you back. Supervise many; only step in where you're called.*
|
|
43
47
|
|
|
44
48
|
## Why you'll want it
|
|
45
49
|
|
|
@@ -214,7 +218,7 @@ Needs **Node ≥ 22.9**, plus these CLIs on your `PATH`:
|
|
|
214
218
|
| **Required** | [`claude`](https://claude.com/claude-code) | every Claude session — this app is a cockpit for it | `npm i -g @anthropic-ai/claude-code`, then run `claude` once to log in |
|
|
215
219
|
| **Required** | `git` | [worktree isolation](#git-worktrees--pull-requests), each cell's branch / unsaved-dot / diff readout, the PR footer | `brew install git` · `sudo apt install git` · `sudo dnf install git` · Windows: [git-scm.com](https://git-scm.com/download/win) |
|
|
216
220
|
| **Required** | `gh` | the cross-repo **PRs & Issues** view and one-click PR creation — it uses your `gh` login, so no token is stored | [cli.github.com](https://cli.github.com), then `gh auth login` |
|
|
217
|
-
| Optional | `glab` | the same for **
|
|
221
|
+
| Optional | `glab` | the same for **GitLab** projects (#981) — gitlab.com, and a self-hosted instance you declare in `gitlabHosts` (#1332). Same arrangement: the CLI holds the credentials, this app stores no token | `brew install glab`, then `glab auth login` (self-hosted: `glab auth login --hostname gitlab.example.com`) |
|
|
218
222
|
| Recommended | `tmux` | [session persistence](#session-persistence-tmux) — terminals survive a server restart | `brew install tmux` · `sudo apt install tmux` · `sudo dnf install tmux` · no native Windows build (falls back to plain PTYs) |
|
|
219
223
|
| Optional | `codex` | [Codex sessions](#agents-claude--codex) in a cell, alongside Claude | `npm i -g @openai/codex` |
|
|
220
224
|
| Optional | `ffmpeg` | video rendering from the [mulmo-script panel](#wiki-collections--the-gui-panel) (its plugin ships enabled) | `brew install ffmpeg` · `sudo apt install ffmpeg` · `sudo dnf install ffmpeg` |
|
|
@@ -386,9 +390,10 @@ today — **Claude Code** (the default), **Codex**, and **Antigravity** (`agy`).
|
|
|
386
390
|
|
|
387
391
|
- **Claude** — spawned as `claude` (override with `CLAUDE_BIN`). The server passes
|
|
388
392
|
`--session-id <uuid>`, so it knows the live session's id even before its transcript
|
|
389
|
-
file exists, and injects activity hooks
|
|
393
|
+
file exists, and injects activity hooks per spawn (see
|
|
390
394
|
[Claude hook injection](#claude-hook-injection)) plus the
|
|
391
395
|
[closing summary](#closing-summary) instruction.
|
|
396
|
+
The **whole** GUI MCP (`--mcp-config` + `--strict-mcp-config`) goes only to a session that is not a grid cell, or to a grid cell whose cwd IS the workspace — `carriesFullGuiMcp` in `server/session/spawn-claude.ts`, which is what gives a workspace cell the tools the single view had before 4.0.0 removed it. That equivalence is about what the session *carries*: a workspace cell is still a grid cell in every other respect. A cell in a project directory attaches neither flag, so Claude Code loads MCP servers the ordinary way — the directory's own local scope, any `.mcp.json` up the tree, and your global ones — including whichever [Canvas switches](#wiki-collections--the-gui-panel) are registered for it.
|
|
392
397
|
- **Codex** — spawned as `codex` (override with `CODEX_BIN`; `CODEX_MODEL` sets
|
|
393
398
|
`--model`). Codex runs on its own WebSocket (`/ws/codex`) and its sessions appear in the
|
|
394
399
|
cockpit roster next to Claude's. Because Codex only mints its rollout id **after** the first
|
|
@@ -543,10 +548,11 @@ The Settings modal (⚙) persists per-user UI choices to `~/.mulmoterminal/confi
|
|
|
543
548
|
| `soundKinds` | Which moments beep — see [Notification sounds](#notification-sounds). Defaults to `["finished","waiting"]`; the other kinds are opt-in. |
|
|
544
549
|
| `sounds` | Per-kind sound: `{ "waiting": "preset:coin" }`. A `preset:<id>` reference or an absolute path; a kind with no entry falls back to `soundFile`. |
|
|
545
550
|
| `prRepos` | `owner/repo` entries whose open PRs/issues the cross-repo **PRs & Issues** view aggregates, using whichever CLI the host needs — your own `gh` or `glab` login, so no token is stored here. An entry may name its host — `gitlab.com/group/project` is read with `glab`, and work can be started on it, commented on and turned into a merge request. A host that is neither shows a row saying so. |
|
|
551
|
+
| `gitlabHosts` | Hosts that run a **self-hosted GitLab**, e.g. `["gitlab.example.com"]`. Nothing in a URL says which forge a host runs, so declaring it is what lets `prRepos` entries on that host be read with `glab` — everything gitlab.com can do, it can do. Needs `glab auth login --hostname <host>`. config.json only (no Settings control), so a hand edit takes effect on the next server start. |
|
|
546
552
|
| `repoDirs` | `{ "owner/repo": "/abs/path" }` — which local clone work on a repo starts in, when you keep several side by side. Only the *choice* is stored; which clones exist is re-derived from `cwdPresets` on every read, and an entry that no longer names a clone of that repo is ignored. |
|
|
547
553
|
| `launchers` | `{ label, command }` entries offered in a grid cell's launcher besides the agents — any interactive command. A plain shell needs no entry: the launch form's **Shell** toggle opens `$SHELL` unconfigured. |
|
|
548
554
|
| `quickCommands` | `{ label, text, agents? }` phrases the **phone** offers as chips on a session's terminal view. Tapping one puts `text` in the input box; it is not sent until you press send. `agents` (`"claude"` / `"codex"` / `"shell"`) scopes a chip to session kinds — omit it to offer the chip everywhere. Empty by default. |
|
|
549
|
-
| `userMcpServers` | `{ id, url }` HTTP MCP servers merged into the `--mcp-config` of the Claude sessions that carry the full GUI MCP — a cell whose working directory is the **workspace**, and
|
|
555
|
+
| `userMcpServers` | `{ id, url }` HTTP MCP servers merged into the `--mcp-config` of the Claude sessions that carry the full GUI MCP — a cell whose working directory is the **workspace**, and a session the server starts itself (the phone, a scheduled task) unless it asks for a grid cell's shape, as an issue's seed session does (`issueSpawnOptions`). A cell in a project directory loads its own MCP config instead. Takes effect on the next session. |
|
|
550
556
|
| `buttons` | Header action buttons — see [Header buttons](#header-buttons). Omit to keep the defaults; set to replace them. |
|
|
551
557
|
| `chips` | Header info chips (`dir` / `git` / `work` / `diff` / `ctx` / `usage` / `status` / `tools`, or custom text). Omit to keep the default set; `[]` hides all built-ins. `work` shows which PR / issue the cell is on (`#977 → #966`) and clears itself when the PR merges — see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#work-chip). |
|
|
552
558
|
| `pushEnabled` | `true` to send a **Web Push** to your registered devices. Off by default; only sends while the **RemoteHost** channel is connected (see below). The master switch — `pushKinds` picks which moments. |
|
|
@@ -706,7 +712,7 @@ malformed file is ignored.
|
|
|
706
712
|
| `theme` | xterm palette for terminals in this directory (one of the built-in theme ids). |
|
|
707
713
|
| `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. |
|
|
708
714
|
| `fontSize` | Terminal font size in px for this directory (8–32), overriding the Settings value. A size outside the range is clamped; a non-number is ignored. Changing it re-fits the terminal, so the PTY learns the new width — unlike browser zoom, which leaves the two disagreeing. |
|
|
709
|
-
| `orderPriority` | This directory's rank in the grid's **priority** ordering — the third mode on the toolbar's ordering button, next to auto (attention-first) and manual (the move buttons). Any integer, **lowest first**; negatives are allowed. Directories that set nothing sort last, keeping their existing order, so adding the key to one project doesn't shuffle the rest. The grid reads it in **priority** mode only; the launcher's directory chips
|
|
715
|
+
| `orderPriority` | This directory's rank in the grid's **priority** ordering — the third mode on the toolbar's ordering button, next to auto (attention-first) and manual (the move buttons). Any integer, **lowest first**; negatives are allowed. Directories that set nothing sort last, keeping their existing order, so adding the key to one project doesn't shuffle the rest. The grid reads it in **priority** mode only; the launcher's directory chips sort by it, so a project sits in the same place on both. The one exception is the **workspace** chip, which always leads the launcher's row regardless of any rank — it is not one of the directories being ranked against each other, and it is the one place every GUI tool is reachable (see [MCP server ids](#mcp-server-ids-why-a-workspace-cell-and-a-project-cell-disagree)). |
|
|
710
716
|
| `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. |
|
|
711
717
|
| `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. |
|
|
712
718
|
| `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. |
|
|
@@ -740,22 +746,21 @@ yarn dev # backend (:34567) + Vite UI (:6856), concurrently — o
|
|
|
740
746
|
yarn dev:server # backend only (node --import tsx --env-file-if-exists=.env server/index.ts)
|
|
741
747
|
yarn dev:client # Vite dev server only
|
|
742
748
|
|
|
743
|
-
yarn
|
|
744
|
-
yarn
|
|
745
|
-
yarn typecheck:test # type-check the specs (tsconfig.test*.json)
|
|
749
|
+
yarn typecheck # type-check everything (vue-tsc -b)
|
|
750
|
+
yarn build # type-check + vite build -> dist/
|
|
746
751
|
yarn server # run backend; serves dist/ + the APIs on :34567
|
|
747
752
|
yarn test # vitest run
|
|
748
753
|
```
|
|
749
754
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
checking
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
755
|
+
`yarn typecheck` covers the whole repo. The root `tsconfig.json` is a solution
|
|
756
|
+
file that references all five projects, so one `vue-tsc -b` builds them:
|
|
757
|
+
`tsconfig.app.json` (client), `tsconfig.node.json` (vite config),
|
|
758
|
+
`tsconfig.server.json` (backend, run directly via `tsx` with no build step),
|
|
759
|
+
plus `tsconfig.test.json` and `tsconfig.test-server.json` for the specs — which
|
|
760
|
+
need checking of their own because vitest strips types rather than checking
|
|
761
|
+
them. They exist as separate projects because each has its own compiler options
|
|
762
|
+
(the client ones DOM + `.vue`, the server ones node, the specs with
|
|
763
|
+
`noUncheckedIndexedAccess` off).
|
|
759
764
|
|
|
760
765
|
In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
|
|
761
766
|
`:34567`. In production, run `yarn build` then `yarn server` and open
|
|
@@ -766,7 +771,22 @@ In dev, open the Vite URL; its proxy forwards `/ws`, `/ws/pubsub`, and `/api` to
|
|
|
766
771
|
## Scripts (Run menu)
|
|
767
772
|
|
|
768
773
|
An empty grid cell's launcher sets the **Working directory** by typing, by a preset
|
|
769
|
-
chip, or with the **📁 folder button** (a native OS folder dialog).
|
|
774
|
+
chip, or with the **📁 folder button** (a native OS folder dialog). The preset chips are
|
|
775
|
+
the directories you have launched in; the **workspace** leads them always, marked with an
|
|
776
|
+
icon, whether or not you have ever launched there — it is the one directory where a
|
|
777
|
+
session reaches every GUI tool, so it is never a click you can lose. It has no remove
|
|
778
|
+
button for the same reason.
|
|
779
|
+
|
|
780
|
+
**The launcher is shorter in the workspace**, because two of its choices do not apply
|
|
781
|
+
there. The per-directory **Canvas switches** are replaced by a line saying every GUI tool
|
|
782
|
+
is already available — a session there is handed the whole GUI MCP at spawn, so switches
|
|
783
|
+
would write a registration that `--strict-mcp-config` then ignores. And the **worktree**
|
|
784
|
+
section is hidden: a worktree isolates work on one codebase onto a branch, while the
|
|
785
|
+
workspace is what a session works *from* (the shared wiki, collections and accounting
|
|
786
|
+
live there), which is precisely what a detached branch would cut it off from. Both come
|
|
787
|
+
back the moment you point the field at a project directory.
|
|
788
|
+
|
|
789
|
+
It also offers a
|
|
770
790
|
**run a script** row
|
|
771
791
|
that launches project scripts (a dev server, tests, a build, …) **in that cell, in
|
|
772
792
|
the directory the cell is pointed at** — so a whole workflow lives in one window
|
|
@@ -920,6 +940,17 @@ separate working tree that shares the repo's `.git`, so several agents can work
|
|
|
920
940
|
repo without colliding. Worktrees live under `~/.mulmoterminal/worktrees/` (override with
|
|
921
941
|
`MULMOTERMINAL_HOME`), and existing ones are listed below the field.
|
|
922
942
|
|
|
943
|
+
**A worktree inherits the project's settings.** `.mulmoterminal.json` is normally gitignored,
|
|
944
|
+
so a fresh worktree used to have none — no colours, no name, no model, no grid rank. It is now
|
|
945
|
+
given its own copy derived from the project's: `name` / `theme` / `colors` / `fontSize` /
|
|
946
|
+
`fontFamily` / `provider` / `model` as written, the seven chrome colours **rotated 12 degrees
|
|
947
|
+
further around the hue wheel per worktree** (so a project's trees read as a gradient; a grey like
|
|
948
|
+
`#ffffff` has no hue to move and stays put), and `orderPriority` at the project's rank **+ 1**, so
|
|
949
|
+
the worktree sorts directly after it. `sound` / `sounds` / `addDirs` are not carried — they name
|
|
950
|
+
paths inside the project directory. Written only where git would **ignore** it: an untracked file
|
|
951
|
+
in a worktree's `git status` would make it count as dirty, and a dirty worktree is one
|
|
952
|
+
MulmoTerminal refuses to remove. An existing config in the worktree is never overwritten.
|
|
953
|
+
|
|
923
954
|
**One worktree, one session.** A worktree is tied to a branch, so it is never started
|
|
924
955
|
twice: a listed row **resumes** that worktree's session when it has one, and **starts** one
|
|
925
956
|
only when it has none. A row whose session is open in another terminal reads `in use` and
|
|
@@ -968,7 +999,7 @@ Closing a worktree cell asks whether to **keep** the worktree or **discard & rem
|
|
|
968
999
|
|
|
969
1000
|
**PRs & Issues (cross-repo).** The toolbar's **Pull requests** button opens a full-screen
|
|
970
1001
|
view that aggregates open PRs **and** issues across the repos listed in Settings →
|
|
971
|
-
**Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project`) via your server-side `gh` / `glab` login.
|
|
1002
|
+
**Pull request repos** (`prRepos`, `owner/repo` entries, or `gitlab.com/group/project` — plus any host declared in `gitlabHosts`) via your server-side `gh` / `glab` login.
|
|
972
1003
|
PRs show a CI-rollup / review-decision / draft badge; each repo lists its latest open
|
|
973
1004
|
issues. Rows are real links, per-repo errors don't sink the view, and the two lists load
|
|
974
1005
|
independently. Backed by `GET /api/prs` and `GET /api/issues`.
|
|
@@ -1035,11 +1066,61 @@ GUI-protocol tools the agent calls — documents (`presentDocument`), forms (`pr
|
|
|
1035
1066
|
generated images, charts, HTML, and collection cards. Each result is drawn by its plugin's
|
|
1036
1067
|
own Vue view inside a Shadow-DOM `PluginFrame` (so a plugin's bundled CSS can't leak),
|
|
1037
1068
|
mirrors the active session, and replays history on re-select. Plugins reach the agent over
|
|
1038
|
-
an **in-process MCP server** served per session at `POST /api/mcp/:sessionId
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1069
|
+
an **in-process MCP server** served per session at `POST /api/mcp/:sessionId`. Which plugins
|
|
1070
|
+
load is gated by `plugins/plugins.json`; the shipped set includes markdown, form, image
|
|
1071
|
+
generation (needs `GEMINI_API_KEY`), chart, HTML, collection, and mulmoscript (MulmoCast
|
|
1072
|
+
video/slides/PDF playback) views. You can also merge your **own HTTP MCP servers** into a
|
|
1073
|
+
workspace session via Settings → `userMcpServers`.
|
|
1074
|
+
|
|
1075
|
+
### MCP server ids: why a workspace cell and a project cell disagree
|
|
1076
|
+
|
|
1077
|
+
A tool is never called by its own name. Every MCP client prefixes it with the id of the server
|
|
1078
|
+
it came from — `mcp__<id>__presentChart` in Claude Code, `mcp-<id>-presentChart` in Codex (which
|
|
1079
|
+
also rewrites `-` in the id to `_`). So the id you register under is repeated on every tool, in
|
|
1080
|
+
every listing, for the life of the session.
|
|
1081
|
+
|
|
1082
|
+
MulmoTerminal delivers the GUI MCP by **two different routes**, and they do not share an id:
|
|
1083
|
+
|
|
1084
|
+
| | Workspace cell / single view | Project-directory grid cell |
|
|
1085
|
+
|---|---|---|
|
|
1086
|
+
| How it arrives | generated per spawn into `--mcp-config` (Claude) or `-c mcp_servers.<id>.url=` (Codex) | the user's OWN per-folder config — `.mcp.json`, `claude mcp add -s local` |
|
|
1087
|
+
| Server id | **`mt`** | **`mulmoterminal-render`**, `-data`, `-media`, `-external` — one per tool group |
|
|
1088
|
+
| Tools carried | all of them, on one URL | only the groups that directory registered |
|
|
1089
|
+
| Tool name looks like | `mcp__mt__presentChart` | `mcp__mulmoterminal-render__presentChart` |
|
|
1090
|
+
|
|
1091
|
+
Which route a session takes is decided by `carriesFullGuiMcp()` in
|
|
1092
|
+
`server/session/mcp-config.ts` — the single view, a cell-less chat, or anything whose cwd **is**
|
|
1093
|
+
the workspace take the first; anything in a project directory takes the second.
|
|
1094
|
+
|
|
1095
|
+
**The workspace is agent-agnostic, and so is the way you start a terminal there.** All three
|
|
1096
|
+
agents and the launcher chips ask the same predicate, so two terminals in the workspace reach
|
|
1097
|
+
the same tools no matter how they were started:
|
|
1098
|
+
|
|
1099
|
+
| Started as | In the workspace | In a project directory |
|
|
1100
|
+
|---|---|---|
|
|
1101
|
+
| claude cell (including `?gui=0`) | `mt`, every tool | the directory's registered groups |
|
|
1102
|
+
| codex cell | `mt`, every tool | the directory's registered groups |
|
|
1103
|
+
| launcher chip running `claude` | `mt`, every tool — flags inserted into the command line | nothing injected; its own `.mcp.json` loads |
|
|
1104
|
+
| launcher chip running `codex` | `mt`, every tool — `-c` overrides inserted | the directory's registered groups |
|
|
1105
|
+
| launcher chip running anything else | untouched | untouched |
|
|
1106
|
+
|
|
1107
|
+
A chip is a command line the user wrote, so the injection is a **rewrite of their text** and is
|
|
1108
|
+
deliberately narrow: only a bare `claude` or `codex` is recognised, and anything else — a wrapper
|
|
1109
|
+
script, `FOO=1 claude` — is passed through unchanged.
|
|
1110
|
+
|
|
1111
|
+
A `claude` chip in the workspace is given `--strict-mcp-config`, same as the cell. That makes the
|
|
1112
|
+
generated config the only source — but it already contains your Settings `userMcpServers`, so
|
|
1113
|
+
**those still load and are still pre-approved**. What stops contributing is a project directory's
|
|
1114
|
+
per-folder `.mcp.json`, which in the workspace is the intent: that file is where the per-group URLs
|
|
1115
|
+
live, and the chip is being handed the all-tools URL instead.
|
|
1116
|
+
|
|
1117
|
+
**The asymmetry is deliberate.** `mt` is ours to name: nothing on disk holds it, it is
|
|
1118
|
+
regenerated on every spawn, so it was shortened to stop paying 17 characters per tool name. The
|
|
1119
|
+
group ids are the opposite — they are keys in config files **users wrote**, they are what the
|
|
1120
|
+
launcher's per-group switch reads back, and they are documented in the setup guide. Renaming
|
|
1121
|
+
them breaks working setups silently, and would need a migration that rewrites existing
|
|
1122
|
+
per-folder configs. Both live in [`common/toolGroups.ts`](common/toolGroups.ts), where the
|
|
1123
|
+
constants carry the same warning.
|
|
1043
1124
|
|
|
1044
1125
|
**Wiki.** The toolbar **Wiki** button opens a read-only browser over `<workspace>/data/wiki/`
|
|
1045
1126
|
— an **index** (tag-filterable page catalog), rendered **pages** with `[[wiki links]]` and
|
|
@@ -1309,7 +1390,7 @@ same-origin-guarded.
|
|
|
1309
1390
|
| `POST /api/git-remote` | The dir's GitHub repo URL (for the header GitHub menu). |
|
|
1310
1391
|
| `GET /api/worktrees?cwd=` · `GET /api/worktrees/diff?cwd=` | List managed worktrees / diff one vs its base. |
|
|
1311
1392
|
| `POST /api/worktrees/create` · `/remove` · `/push` · `/pr` | Create on `agent/<slug>` — or, with `issue: <N>`, on `issue/<N>-<slug>` forked from a freshly fetched `origin/<base>`; remove (managed root only), push, open a PR (`gh`, else compare URL). |
|
|
1312
|
-
| `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for
|
|
1393
|
+
| `GET /api/prs` · `GET /api/issues` | Open PRs / issues across the configured `prRepos` — `gh` for github.com entries, `glab` for gitlab.com and any host declared in `gitlabHosts`. |
|
|
1313
1394
|
| `GET /api/repo-dirs` | Which saved directories clone which GitHub repo, ordered, with the recorded choice per repo. |
|
|
1314
1395
|
| `POST /api/issues/start` | Cut an issue's worktree in one of that repo's known clones and spawn a session there, seeded with the issue as a draft. |
|
|
1315
1396
|
| `GET /api/github/star` · `POST /api/github/star` | Whether you have starred MulmoTerminal, and star it (via `gh`). `starred: null` means `gh` could not answer, and hides the button. |
|
|
@@ -1719,6 +1800,19 @@ and `fetch` are mocked so the tests run without a server.
|
|
|
1719
1800
|
|
|
1720
1801
|
---
|
|
1721
1802
|
|
|
1803
|
+
## Who builds this
|
|
1804
|
+
|
|
1805
|
+
MulmoTerminal is built by the **[Singularity Society](https://singularitysociety.org)** team,
|
|
1806
|
+
including **[Satoshi Nakajima](https://x.com/snakajima)** — software architect for **Windows 95**,
|
|
1807
|
+
**Windows 98** and **Internet Explorer 3.0 / 4.0** at Microsoft, later founder of UIEvolution /
|
|
1808
|
+
Xevo, and still building from Seattle.
|
|
1809
|
+
|
|
1810
|
+
It exists because we run several coding agents every day and kept losing track of which one was
|
|
1811
|
+
waiting on us. Everything here was built for that, then kept because it worked. MIT licensed.
|
|
1812
|
+
|
|
1813
|
+
- **Updates** are announced in Japanese on X: [@SingularitySoci](https://x.com/SingularitySoci)
|
|
1814
|
+
- **Sister project:** [MulmoClaude](https://github.com/receptron/mulmoclaude)
|
|
1815
|
+
|
|
1722
1816
|
## Contributing
|
|
1723
1817
|
|
|
1724
1818
|
**Please open an issue rather than a pull request.** Bug reports and feature requests are very
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Sort filenames and directory names deterministically.
|
|
2
|
+
//
|
|
3
|
+
// `.sort()` with no comparator is flagged (sonarjs/no-alphabetical-sort) because the default is
|
|
4
|
+
// implementation-defined for non-ASCII. The rule suggests `localeCompare`, which would be WRONG
|
|
5
|
+
// here: these are filenames — zero-padded date directories and ISO-stamped rollout files — and
|
|
6
|
+
// ordering them by the user's locale makes the answer depend on who is running the app. Code-unit
|
|
7
|
+
// order is what the callers actually mean, and it is the same everywhere.
|
|
8
|
+
export function byCodeUnit(a: string, b: string): number {
|
|
9
|
+
if (a < b) return -1;
|
|
10
|
+
return a > b ? 1 : 0;
|
|
11
|
+
}
|
package/common/codeBlocks.ts
CHANGED
|
@@ -59,8 +59,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
|
|
|
59
59
|
const lines = markdown.split("\n");
|
|
60
60
|
const blocks: FencedBlock[] = [];
|
|
61
61
|
let i = 0;
|
|
62
|
+
// Each line is read into a const before use: with noUncheckedIndexedAccess, `lines[i]` is
|
|
63
|
+
// `string | undefined` even inside `i < lines.length`, and the loop condition is what actually
|
|
64
|
+
// rules the undefined out.
|
|
62
65
|
while (i < lines.length) {
|
|
63
|
-
const
|
|
66
|
+
const line = lines[i];
|
|
67
|
+
const open = line === undefined ? null : fenceOf(line);
|
|
64
68
|
if (!open) {
|
|
65
69
|
i++;
|
|
66
70
|
continue;
|
|
@@ -69,12 +73,12 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
|
|
|
69
73
|
const info = open.info.trim();
|
|
70
74
|
const body: string[] = [];
|
|
71
75
|
i++;
|
|
72
|
-
|
|
73
|
-
body.push(
|
|
76
|
+
for (let next = lines[i]; next !== undefined && !closes(next, fence); next = lines[i]) {
|
|
77
|
+
body.push(next);
|
|
74
78
|
i++;
|
|
75
79
|
}
|
|
76
80
|
i++; // step over the closing fence (or past the end, for an unclosed block)
|
|
77
|
-
blocks.push({ lang: info ? (info.split(/\s+/)[0]
|
|
81
|
+
blocks.push({ lang: info ? (info.split(/\s+/)[0]?.toLowerCase() ?? null) : null, body: body.join("\n") });
|
|
78
82
|
}
|
|
79
83
|
return blocks;
|
|
80
84
|
}
|
|
@@ -84,5 +88,5 @@ export function fencedBlocks(markdown: string): FencedBlock[] {
|
|
|
84
88
|
* exactly like a copy that silently failed. */
|
|
85
89
|
export function lastFencedBlock(markdown: string): FencedBlock | null {
|
|
86
90
|
const withBody = fencedBlocks(markdown).filter((b) => b.body.trim() !== "");
|
|
87
|
-
return withBody
|
|
91
|
+
return withBody[withBody.length - 1] ?? null;
|
|
88
92
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Which hosts are a GitLab this app can talk to (#1332). `gitlab.com` is known from its name; a
|
|
2
|
+
// self-hosted instance at `gitlab.hogefuga.com` is indistinguishable from any other host, so the
|
|
3
|
+
// user DECLARES it in `gitlabHosts` — the config mechanism #981 said this would need.
|
|
4
|
+
//
|
|
5
|
+
// In `common/` because both sides decide from it: the server picks a CLI from it, and the browser
|
|
6
|
+
// says why a control is off. Two copies would drift, and disagreeing is the failure the forge
|
|
7
|
+
// abstraction exists to undo.
|
|
8
|
+
import { GITHUB_HOST, GITLAB_HOST } from "./repoEntry.js";
|
|
9
|
+
|
|
10
|
+
// A hostname, lower case, with at least one dot. The dot is not cosmetic: `parseRepoEntry` reads a
|
|
11
|
+
// leading segment as a host ONLY when it contains one, so a dotless declaration could never match
|
|
12
|
+
// an entry and would sit in the config doing nothing.
|
|
13
|
+
const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
|
|
14
|
+
|
|
15
|
+
const SCHEME_RE = /^https?:\/\//;
|
|
16
|
+
|
|
17
|
+
const GITLAB_HOSTS_MAX = 20;
|
|
18
|
+
|
|
19
|
+
/** One declared host, or null when the string does not name one.
|
|
20
|
+
*
|
|
21
|
+
* A pasted `https://gitlab.example.com/` is accepted and reduced to its hostname: this value is
|
|
22
|
+
* hand-typed into config.json with no UI to validate it, and the browser's address bar is where
|
|
23
|
+
* the user is copying from. Anything else with a slash is rejected rather than truncated — a
|
|
24
|
+
* project path here is a different mistake, and silently keeping its host would hide it.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeGitlabHost(input: unknown): string | null {
|
|
27
|
+
if (typeof input !== "string") return null;
|
|
28
|
+
const host = input.trim().toLowerCase().replace(SCHEME_RE, "").replace(/\/$/, "");
|
|
29
|
+
return HOSTNAME_RE.test(host) ? host : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The declared hosts, de-duplicated and capped. Anything unusable is dropped rather than failing
|
|
33
|
+
* the whole config — one typo must not cost the user the hosts they spelled correctly. */
|
|
34
|
+
export function sanitizeGitlabHosts(input: unknown): string[] {
|
|
35
|
+
if (!Array.isArray(input)) return [];
|
|
36
|
+
const hosts = new Set<string>();
|
|
37
|
+
for (const value of input) {
|
|
38
|
+
const host = normalizeGitlabHost(value);
|
|
39
|
+
if (host) hosts.add(host);
|
|
40
|
+
if (hosts.size >= GITLAB_HOSTS_MAX) break;
|
|
41
|
+
}
|
|
42
|
+
return [...hosts];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether `host` is a GitLab: gitlab.com, or one the user declared.
|
|
46
|
+
*
|
|
47
|
+
* `github.com` is refused whatever the config says. A declaration is a hand-typed line with no UI
|
|
48
|
+
* behind it, and the one that would do real damage — every GitHub repo suddenly addressed with
|
|
49
|
+
* `glab` — is also the easiest to write by accident.
|
|
50
|
+
*/
|
|
51
|
+
export const isGitlabHost = (host: string, declared: readonly string[]): boolean => host === GITLAB_HOST || (host !== GITHUB_HOST && declared.includes(host));
|
|
52
|
+
|
|
53
|
+
/** Why a host is not handled, and what to do about it.
|
|
54
|
+
*
|
|
55
|
+
* ONE sentence, shared by the PRs/Issues row and the start-work refusal: #1332 was filed by a user
|
|
56
|
+
* who read "is not supported yet" and could not tell whether their setup was wrong, whether the
|
|
57
|
+
* feature was coming, or what to try — so the reason has to carry the fix.
|
|
58
|
+
*/
|
|
59
|
+
export const unknownForgeReason = (host: string): string =>
|
|
60
|
+
`${host} is not supported yet — MulmoTerminal reads github.com and gitlab.com; if ${host} is a self-hosted GitLab, add it to "gitlabHosts" in ~/.mulmoterminal/config.json and restart`;
|
package/common/isRecord.ts
CHANGED
|
@@ -11,3 +11,9 @@ import { isObject } from "graphai";
|
|
|
11
11
|
// graphai's own `isPlainObject` is exactly this (it also rejects Map/Date/class instances), but
|
|
12
12
|
// it is not exported from the package root, so the array check stays here.
|
|
13
13
|
export const isRecord = (value: unknown): value is Record<string, unknown> => isObject(value) && !Array.isArray(value);
|
|
14
|
+
|
|
15
|
+
// "Absent, or of this type" — what an OPTIONAL field on a wire shape has to satisfy. A guard that
|
|
16
|
+
// skips these asserts a type it has not seen: absent is meaningful (an older server never said the
|
|
17
|
+
// field), but a present one of the wrong type is a lie the rest of the code then reads.
|
|
18
|
+
export const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
|
|
19
|
+
export const optionalBoolean = (value: unknown): boolean => value === undefined || typeof value === "boolean";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The array counterpart to isRecord, and it exists for a reason that is easy to miss:
|
|
2
|
+
// `Array.isArray(value)` on an `unknown` narrows it to **`any[]`**, not `unknown[]`. So the
|
|
3
|
+
// obvious `Array.isArray(x) ? x : []` hands back an array whose elements are outside the type
|
|
4
|
+
// checker entirely, and every read off them type-checks against anything.
|
|
5
|
+
//
|
|
6
|
+
// Declaring the guard's result as `unknown[]` keeps the elements unverified-but-visible, which is
|
|
7
|
+
// what the callers want: they check each element before using it.
|
|
8
|
+
export const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value);
|
package/common/issueStartPlan.ts
CHANGED
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
// docs/remote-host-protocol.md), so it refuses `choose` where the desktop opens it.
|
|
9
9
|
import type { RepoDirs } from "./repoDirs";
|
|
10
10
|
import { GITHUB_HOST, GITLAB_HOST, parseRepoEntry } from "./repoEntry";
|
|
11
|
+
import { isGitlabHost, unknownForgeReason } from "./gitlabHosts";
|
|
11
12
|
|
|
12
13
|
// Where work can be STARTED — the issue is read and a worktree cut. A host that is merely listable
|
|
13
|
-
// still refuses, and says which host it is rather than blaming a missing clone (#981).
|
|
14
|
-
|
|
14
|
+
// still refuses, and says which host it is rather than blaming a missing clone (#981). A host the
|
|
15
|
+
// user declared as self-hosted GitLab starts work like gitlab.com does (#1332).
|
|
16
|
+
const isStartableHost = (host: string, gitlabHosts: readonly string[]): boolean => host === GITHUB_HOST || isGitlabHost(host, gitlabHosts);
|
|
15
17
|
|
|
16
|
-
/** The hosts named in a refusal,
|
|
17
|
-
* "github.com only" survived GitLab becoming startable and told users the opposite of
|
|
18
|
-
* (Codex review). Exported so the phone's refusal says the same thing. */
|
|
19
|
-
export const startableHosts = (): string => [...
|
|
18
|
+
/** The hosts named in a refusal, built from the same rule rather than written out beside it — a
|
|
19
|
+
* hardcoded "github.com only" survived GitLab becoming startable and told users the opposite of
|
|
20
|
+
* the truth (Codex review). Exported so the phone's refusal says the same thing. */
|
|
21
|
+
export const startableHosts = (gitlabHosts: readonly string[]): string => [...new Set([GITHUB_HOST, GITLAB_HOST, ...gitlabHosts])].join(" and ");
|
|
20
22
|
|
|
21
23
|
export type IssueStartPlan =
|
|
22
24
|
/** No clone of this repo on this machine, so there is nowhere for the work to happen. */
|
|
@@ -35,24 +37,29 @@ export type BlockedIssueStartPlan = Exclude<IssueStartPlan, { kind: "ready" }>;
|
|
|
35
37
|
|
|
36
38
|
// `repo` is REQUIRED, not optional, even though only one branch reads it: a caller that omitted it
|
|
37
39
|
// would get `no-clone` for a GitLab repo, which is the exact wrong answer this parameter was added
|
|
38
|
-
// to fix. Making the compiler ask for it is what stops that coming back.
|
|
39
|
-
|
|
40
|
+
// to fix. Making the compiler ask for it is what stops that coming back. `gitlabHosts` is required
|
|
41
|
+
// for the same reason — defaulted to empty, a caller that forgot it would refuse the user's own
|
|
42
|
+
// self-hosted GitLab and blame the host (#1332).
|
|
43
|
+
export function issueStartPlan(entry: RepoDirs | undefined, repo: string, gitlabHosts: readonly string[]): IssueStartPlan {
|
|
40
44
|
// Checked before the clone list, because a GitLab repo has no entry there and would otherwise
|
|
41
45
|
// read as "no clone" — a reason that is not true and points at the wrong fix.
|
|
42
46
|
const parsed = parseRepoEntry(repo);
|
|
43
|
-
if (parsed && !
|
|
47
|
+
if (parsed && !isStartableHost(parsed.host, gitlabHosts)) return { kind: "unsupported-forge", host: parsed.host };
|
|
44
48
|
const dirs = entry?.dirs ?? [];
|
|
45
49
|
if (dirs.length === 0) return { kind: "no-clone" };
|
|
46
50
|
// A recording wins even when the repo has one clone: it is still the same answer, and treating
|
|
47
51
|
// the two cases the same keeps "recorded" from being a state the UI has to distinguish.
|
|
48
52
|
const recorded = entry?.primary;
|
|
49
53
|
if (recorded) return { kind: "ready", dir: recorded };
|
|
50
|
-
|
|
54
|
+
const only = dirs.length === 1 ? dirs[0] : undefined;
|
|
55
|
+
if (only) return { kind: "ready", dir: only.path };
|
|
51
56
|
return { kind: "choose", dirs };
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/** Why the control is disabled, for the row's title text. Null when it is not disabled. */
|
|
55
60
|
export function issueStartBlockedReason(plan: IssueStartPlan, repo: string): string | null {
|
|
56
|
-
|
|
61
|
+
// The same sentence the row's own error carries: the host is not one this app knows, and the fix
|
|
62
|
+
// is a config line rather than anything about this issue or this clone (#1332).
|
|
63
|
+
if (plan.kind === "unsupported-forge") return unknownForgeReason(plan.host);
|
|
57
64
|
return plan.kind === "no-clone" ? `No local clone of ${repo} — add one to your directory presets to start work here` : null;
|
|
58
65
|
}
|
package/common/keymap.ts
CHANGED
|
@@ -87,6 +87,7 @@ export function parseKeyBinding(input: string): KeyBinding | null {
|
|
|
87
87
|
const parts = input.split("+").map((part) => part.trim());
|
|
88
88
|
if (parts.length === 0 || parts.some((part) => part === "")) return null;
|
|
89
89
|
const key = parts[parts.length - 1];
|
|
90
|
+
if (key === undefined) return null; // unreachable: parts.length was checked above
|
|
90
91
|
const binding: KeyBinding = { key, shift: false, alt: false, ctrl: false, meta: false };
|
|
91
92
|
for (const part of parts.slice(0, -1)) {
|
|
92
93
|
const flag = MODIFIERS[part.toLowerCase()];
|
|
@@ -226,6 +227,7 @@ function duplicateWarnings(bound: Map<string, Claim[]>): KeymapProblem[] {
|
|
|
226
227
|
return [...bound.values()].flatMap((claims) => {
|
|
227
228
|
if (claims.length < 2) return [];
|
|
228
229
|
const [winner, ...losers] = [...claims].sort((a, b) => a.rank - b.rank);
|
|
230
|
+
if (!winner) return []; // unreachable: claims.length >= 2 was checked above
|
|
229
231
|
return losers.map(({ label, binding }) => ({
|
|
230
232
|
action: label,
|
|
231
233
|
binding,
|
package/common/median.ts
CHANGED
|
@@ -8,6 +8,11 @@ export const median = (values: number[]): number | null => {
|
|
|
8
8
|
if (values.length === 0) return null;
|
|
9
9
|
const sorted = [...values].sort((a, b) => a - b);
|
|
10
10
|
const mid = Math.floor(sorted.length / 2);
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Read once each: `sorted[mid]` is `number | undefined` under noUncheckedIndexedAccess, and the
|
|
12
|
+
// length checks above are what actually rule that out.
|
|
13
|
+
const middle = sorted[mid];
|
|
14
|
+
if (middle === undefined) return null;
|
|
15
|
+
if (sorted.length % 2 === 1) return middle;
|
|
16
|
+
const below = sorted[mid - 1];
|
|
17
|
+
return below === undefined ? middle : (below + middle) / 2;
|
|
13
18
|
};
|
package/common/prPhase.ts
CHANGED
|
@@ -83,7 +83,7 @@ function toIssueNumber(digits: string): number | null {
|
|
|
83
83
|
// shown next to this cell's PR has to belong to the same repo to be clickable and true.
|
|
84
84
|
export function issueRefFromPrBody(body: string | null | undefined): number | null {
|
|
85
85
|
const found = typeof body === "string" ? CLOSING_KEYWORD.exec(body) : null;
|
|
86
|
-
return found ? toIssueNumber(found[1])
|
|
86
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Whether the body ALREADY states what merging it closes, in either form GitHub honours. Broader
|
|
@@ -105,7 +105,7 @@ const BRANCH_ISSUE = /^[a-z][a-z-]*\/([1-9]\d*)-/;
|
|
|
105
105
|
// somebody else's issue. Named for the doubt so a call site can't forget it.
|
|
106
106
|
export function issueCandidateFromBranch(branch: string | null | undefined): number | null {
|
|
107
107
|
const found = typeof branch === "string" ? BRANCH_ISSUE.exec(branch) : null;
|
|
108
|
-
return found ? toIssueNumber(found[1])
|
|
108
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// The prefix this app gives a branch it creates FOR an issue (#1171). Shared because the two
|
|
@@ -121,5 +121,5 @@ const ANCHORED_ISSUE = new RegExp(`^${ISSUE_BRANCH_PREFIX}([1-9]\\d*)-`);
|
|
|
121
121
|
// else's issue the moment the PR merges, which no amount of confirming afterwards undoes.
|
|
122
122
|
export function issueFromAnchoredBranch(branch: string | null | undefined): number | null {
|
|
123
123
|
const found = typeof branch === "string" ? ANCHORED_ISSUE.exec(branch) : null;
|
|
124
|
-
return found ? toIssueNumber(found[1])
|
|
124
|
+
return found?.[1] === undefined ? null : toIssueNumber(found[1]);
|
|
125
125
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Reading a string out of untrusted JSON — a phone's command params, a transcript line, a `gh`
|
|
2
|
+
// response — without `String()`.
|
|
3
|
+
//
|
|
4
|
+
// `String(value)` answers for ANY input, and that is the problem: an object becomes the literal
|
|
5
|
+
// text "[object Object]" and an array becomes "1,2". Nothing throws, so the bad value travels —
|
|
6
|
+
// as a collection slug that matches nothing, or as a session title on screen. The rule that
|
|
7
|
+
// catches this is @typescript-eslint/no-base-to-string.
|
|
8
|
+
//
|
|
9
|
+
// Reading returns "" (or null) instead, which the callers already handle: they were written
|
|
10
|
+
// against `String(x ?? "")`, so an absent field was always the empty string.
|
|
11
|
+
|
|
12
|
+
/** The value when it is a string, else `""`. For a field the caller then validates or defaults. */
|
|
13
|
+
export const readString = (value: unknown): string => (typeof value === "string" ? value : "");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A value rendered for a HUMAN — an error message naming what was rejected.
|
|
17
|
+
*
|
|
18
|
+
* Here the input is bad by definition, so there is something to say about any of it; `String()` is
|
|
19
|
+
* wrong only because "[object Object]" tells the reader nothing. JSON keeps the shape visible.
|
|
20
|
+
*/
|
|
21
|
+
export const describeValue = (value: unknown): string => {
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(value) ?? String(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return "[unserializable]"; // a circular structure, which JSON.stringify throws on
|
|
27
|
+
}
|
|
28
|
+
};
|
package/common/repoEntry.ts
CHANGED
|
@@ -30,8 +30,9 @@ export interface RepoEntry {
|
|
|
30
30
|
export function parseRepoEntry(entry: string): RepoEntry | null {
|
|
31
31
|
const segments = entry.trim().split("/");
|
|
32
32
|
if (segments.some((segment) => segment === "")) return null;
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const first = segments[0];
|
|
34
|
+
const host = segments.length > 1 && first !== undefined && HOST_SEGMENT.test(first) ? first : null;
|
|
35
|
+
return host === null ? { host: GITHUB_HOST, path: segments, declared: false } : { host: host.toLowerCase(), path: segments.slice(1), declared: true };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/** The identity a repository is known by ON ITS OWN HOST — `owner/repo` for GitHub, the group path
|
package/common/toolGroups.ts
CHANGED
|
@@ -96,8 +96,45 @@ export const toolsInGroup = (group: ToolGroup): string[] => [...GROUP_BY_TOOL].f
|
|
|
96
96
|
// `mcp__<server id>__<tool>`, and the id comes from the USER's config key — so this is a
|
|
97
97
|
// convention the enable-it-for-this-folder affordance has to write, and a user who registers
|
|
98
98
|
// the same URL under another name simply gets permission prompts (nothing breaks).
|
|
99
|
+
//
|
|
100
|
+
// DELIBERATELY long, and deliberately NOT the same id as GUI_SERVER_ID below. The two are
|
|
101
|
+
// reached by different routes (see that constant), so the same tool is named
|
|
102
|
+
// `mcp__mulmoterminal-render__presentChart` in a project cell and `mcp__mt__presentChart` in a
|
|
103
|
+
// workspace one. That is not drift to be tidied up: shortening these would break every
|
|
104
|
+
// `.mcp.json` and `claude mcp add -s local` entry users have already written against them,
|
|
105
|
+
// which the launcher's per-group switch also reads back and the setup guide documents. It needs
|
|
106
|
+
// a migration over existing per-folder configs, not a rename. README's "MCP server ids" section
|
|
107
|
+
// is the long version.
|
|
99
108
|
export const toolGroupServerId = (group: ToolGroup): string => `mulmoterminal-${group}`;
|
|
100
109
|
|
|
110
|
+
// The MCP server id the SINGLE VIEW registers — every tool on one URL, rather than the per-group
|
|
111
|
+
// ids above. Unlike those, a user never writes this one: it is generated per spawn into
|
|
112
|
+
// `--mcp-config` (claude) or `-c mcp_servers.<id>.url=` (codex), so it is ours to name.
|
|
113
|
+
//
|
|
114
|
+
// Which of the two ids a session gets is decided by `carriesFullGuiMcp` in
|
|
115
|
+
// server/session/spawn-claude.ts: the single view, a cell-less chat, and a cell whose cwd IS the
|
|
116
|
+
// workspace carry this one; a cell in a project directory is handed no --mcp-config at all and
|
|
117
|
+
// reaches the group ids through the user's own per-folder config. Both are live, on purpose.
|
|
118
|
+
//
|
|
119
|
+
// It is SHORT because the id is not what the agent sees — the client prefixes every tool with it.
|
|
120
|
+
// claude turns `presentChart` into `mcp__<id>__presentChart` and codex into `mcp-<id>-presentChart`
|
|
121
|
+
// (with `-` in the id normalised to `_`), so a long id is paid on every tool name in every listing.
|
|
122
|
+
// `mulmoterminal-gui` cost 17 characters per tool to say what the surrounding config already says.
|
|
123
|
+
//
|
|
124
|
+
// It had been spelled out at four sites (this server's registration, the advertised server name,
|
|
125
|
+
// the allowedTools prefix and the reserved-id list). It lives here now because both the server and
|
|
126
|
+
// the UI-facing config validation decide from it — see the `common/` rule in CLAUDE.md.
|
|
127
|
+
export const GUI_SERVER_ID = "mt";
|
|
128
|
+
|
|
129
|
+
// Ids this project has shipped for the SAME single-view server before. Not live: kept so the place
|
|
130
|
+
// that must recognise our own past output still does — the Antigravity config merge, which deletes
|
|
131
|
+
// our entries by id and would otherwise leave a stale `mulmoterminal-gui` behind forever.
|
|
132
|
+
//
|
|
133
|
+
// Only where we WROTE them, though. A legacy id is not reserved against the user's own
|
|
134
|
+
// `userMcpServers`: nothing writes it any more, so a server someone names `mulmoterminal-gui`
|
|
135
|
+
// today is reachable and works, and treating it as ours would be claiming a name we abandoned.
|
|
136
|
+
export const LEGACY_GUI_SERVER_IDS: readonly string[] = ["mulmoterminal-gui"];
|
|
137
|
+
|
|
101
138
|
// The tools MulmoTerminal pre-approves via `--allowedTools`, so they run without a permission
|
|
102
139
|
// prompt. A list of TOOLS, not of groups: a group says which tools a directory can reach, and
|
|
103
140
|
// that is not the same question as which may run unattended.
|
|
@@ -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-DxEa8E3F-
|
|
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-DxEa8E3F-D6oVwac4.js";import"./chunk-WYO6CB5R-BlzOfotS-DMj-dLn7.js";import"./chunk-VAUOI2AC-CS9QJ4yz-DDJgABqC.js";import{n as i,r as a,t as o}from"./chunk-MOJQB5TN-CMZRaeqt-ePkETRCM.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-DxEa8E3F-D6oVwac4.js";export{e as createArchitectureServices};
|