collabmd 0.1.48 → 0.1.49
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 +61 -5
- package/bin/collabmd.js +3 -0
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-C-nmgL5g.js +1 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-CdVZqicp.js +1 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-I39sYjHI.js → architectureDiagram-5GKGNRK7-wUo0bXif.js} +3 -3
- package/dist/client/assets/{blockDiagram-NRAW4CY4-C6DtJy5Y.js → blockDiagram-I7D4REHJ-BRSK3m49.js} +4 -4
- package/dist/client/assets/browser-utils-DbXSMI-6.js +1 -0
- package/dist/client/assets/{c4Diagram-UCG6FXSJ-CtWtxS9_.js → c4Diagram-7LVT6UL2-yD2poxqw.js} +1 -1
- package/dist/client/assets/channel-Fa4GS_-F.js +1 -0
- package/dist/client/assets/chunk-2E4U76K2-BPnTT-J3.js +2 -0
- package/dist/client/assets/{chunk-TEH6E4GO-DVayPj-q.js → chunk-4HAMMTFA-DStbIfJ1.js} +20 -20
- package/dist/client/assets/{chunk-75Z2AOVW-CdFSaPjz.js → chunk-75Z2AOVW-C5pGd3W9.js} +1 -1
- package/dist/client/assets/{chunk-CLGD4ZFX-DQLgbQrv.js → chunk-CLGD4ZFX-BMeArk03.js} +1 -1
- package/dist/client/assets/chunk-DU6HZSFF-Zkp69c6c.js +127 -0
- package/dist/client/assets/{chunk-F27PBJKO-CNHIFhjo.js → chunk-F27PBJKO-D4xNct1r.js} +1 -1
- package/dist/client/assets/{chunk-GMAD6QVW-CbcBqPM5.js → chunk-GMAD6QVW-NZ6WpDb8.js} +1 -1
- package/dist/client/assets/{chunk-OBVCFTLP-BJR7u1jh.js → chunk-GVQU2GXP-D8SDprfX.js} +1 -1
- package/dist/client/assets/{chunk-G27WJ6UU-D9-eg537.js → chunk-IMKFNOWR-D8jEuU3Q.js} +2 -2
- package/dist/client/assets/{chunk-JQ64N6SF-DSFNH4bh.js → chunk-L3NEJ4N5-aijKP-eh.js} +1 -1
- package/dist/client/assets/chunk-OSK3NFVY-CCbLQAMa.js +10 -0
- package/dist/client/assets/{chunk-P2QGCYS3-BRpqY32H.js → chunk-P2QGCYS3-Oag_FdCq.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-Cb4SlVZF.js → chunk-POPQ4Y6H-CYnpIgXT.js} +1 -1
- package/dist/client/assets/{chunk-PWAF6VOD-Cz8IwBB5.js → chunk-PWAF6VOD-JSNrcBmP.js} +1 -1
- package/dist/client/assets/{chunk-RM4UPSZO-CWRwxNWJ.js → chunk-RM4UPSZO-DOWllDY8.js} +4 -4
- package/dist/client/assets/{chunk-RHFEMEQ7-CWY8cf4H.js → chunk-SHT3W25Y-BiQGljao.js} +5 -5
- package/dist/client/assets/{chunk-SVP7TREG-CyHOmcac.js → chunk-SVP7TREG-DB485R5x.js} +1 -1
- package/dist/client/assets/{chunk-LCL6LL3I-DmDkVukh.js → chunk-TICWLB2K-8rAVvoS3.js} +4 -4
- package/dist/client/assets/{chunk-CKBBP62Z-DG26UiBv.js → chunk-TLUHSLCS-DTY5v0aj.js} +2 -2
- package/dist/client/assets/classDiagram-ZZMXUADV-BsQWoR3Q.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-BsQWoR3Q.js +1 -0
- package/dist/client/assets/{comment-thread-store-CdcwrObc.js → comment-thread-store-CX98zR6q.js} +1 -1
- package/dist/client/assets/comment-threads-BnRwo5B9.js +4 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-CdTPi2yO.js → cynefinDiagram-5FMLGOSQ-rTz9RhFH.js} +1 -1
- package/dist/client/assets/{dagre-3AP2YEHR-5gtJtd0O.js → dagre-GXQ25YYZ-Cr67wLpQ.js} +2 -2
- package/dist/client/assets/{diagram-S7CK7UJ4-BudWz9WU.js → diagram-S7CK7UJ4-DywERe__.js} +2 -2
- package/dist/client/assets/{diagram-UQ7AKVKN-3v4rbH-s.js → diagram-UQ7AKVKN-CNYlXtu4.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-0H1a61Bn.js +3 -0
- package/dist/client/assets/{diagram-VX7I27RA-C_-rj0sw.js → diagram-VX7I27RA-CsJbsx5F.js} +1 -1
- package/dist/client/assets/{diagram-Z3DM3KII-B4yxavgj.js → diagram-Z3DM3KII-D0mLxEgI.js} +1 -1
- package/dist/client/assets/dist--hk9I5HQ.js +1 -0
- package/dist/client/assets/{dist-DutPuFmC.js → dist-B1XEa-Zs.js} +1 -1
- package/dist/client/assets/dist-Bd1JucQF.js +1 -0
- package/dist/client/assets/{dist-DZAAWTG-.js → dist-C0G3AaXG.js} +1 -1
- package/dist/client/assets/dist-Dm4w5UId.js +1 -0
- package/dist/client/assets/{dist-B_yQaEv_.js → dist-DpYDHR9p.js} +1 -1
- package/dist/client/assets/{dist-CietwTVS.js → dist-DzNmC81X.js} +1 -1
- package/dist/client/assets/{dist-DDUn5zTz.js → dist-GvvCNBe4.js} +1 -1
- package/dist/client/assets/dist-JaU3aRlD.js +19 -0
- package/dist/client/assets/{dist-Bw3VIE_v.js → dist-TW8iFRyW.js} +1 -1
- package/dist/client/assets/{dist-Dir5dDKZ.js → dist-jVU5Be5P.js} +1 -1
- package/dist/client/assets/document-formatter-rDDjRfZ2.js +2 -0
- package/dist/client/assets/drawioEditor-JubEEha7.js +9 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-HeTxHpCU.js +1 -0
- package/dist/client/assets/editor-session-Bk6oIvSr.js +54 -0
- package/dist/client/assets/{embedded-editor-base-B3l6Xy65.css → embedded-editor-base-BlCgz6mw.css} +1 -1
- package/dist/client/assets/{erDiagram-SSCWMZ5O-C-iy6VkX.js → erDiagram-RLTQ6QDP-CbrDdd5P.js} +2 -2
- package/dist/client/assets/{vault-utils-DvoSftJT.js → es-Bfdp096f.js} +13 -13
- package/dist/client/assets/excalidraw-element-link-YLIaTnVF.js +7 -0
- package/dist/client/assets/{excalidraw-scene-BOben2gr.js → excalidraw-scene-6uROebca.js} +1 -1
- package/dist/client/assets/excalidrawEditor-dBMbRX_S.js +9 -0
- package/dist/client/assets/export-page-CRRQ2NEl.js +57 -0
- package/dist/client/assets/exportDocument-BlQRd9_m.js +2 -0
- package/dist/client/assets/exportDocument-CNSbjZwu.css +1 -0
- package/dist/client/assets/flowDiagram-HODETNUW-D5Gy0wmg.js +1 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-DtKHAbez.js → ganttDiagram-EL5Y4UJY-BHgOYiZz.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-oue8fQ5o.js → gitGraphDiagram-WWUBYQGX-Dl__X3Sk.js} +1 -1
- package/dist/client/assets/image-ZVKINSPA-DgzW5zAy.js +1 -0
- package/dist/client/assets/index-D3RER1SZ.js +1023 -0
- package/dist/client/assets/index-DREVKl_K.css +1 -0
- package/dist/client/assets/{infoDiagram-RXCK75RN-BMzo6KIn.js → infoDiagram-27XIBGKW-DBpoANC9.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BIM3h287.js → ishikawaDiagram-5VMMS53U-BMPC6hfp.js} +1 -1
- package/dist/client/assets/{journeyDiagram-EYS64GPL-mF6Lh8O-.js → journeyDiagram-3NMN7TZE-B1sApUmM.js} +2 -2
- package/dist/client/assets/{kanban-definition-3QL26DDD-DLN2NyoR.js → kanban-definition-UXKFOSKX-C9yP4td1.js} +4 -4
- package/dist/client/assets/{line-BfTWJmgf.js → line-BFl3ujec.js} +1 -1
- package/dist/client/assets/mermaid.core-BrY09czX.js +11 -0
- package/dist/client/assets/mermaid.core-DfcNfOTv.js +1 -0
- package/dist/client/assets/{mindmap-definition-FBJOCRG2-CRyHaHTk.js → mindmap-definition-YA3MSWOX-BUQHcYbK.js} +8 -8
- package/dist/client/assets/pegDiagram-XKGWAZYB-Bi2Iml7Z.js +1 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-B5OZPb88.js → pieDiagram-E7YTZNPT-_gYXnMoi.js} +1 -1
- package/dist/client/assets/preview-render-compiler-CfxJYldJ.js +41 -0
- package/dist/client/assets/preview-render-compiler-Cu8_i9iS.js +1 -0
- package/dist/client/assets/{preview-render-worker-_QTxqKvP.js → preview-render-worker-c4Yi93qW.js} +41 -35
- package/dist/client/assets/prod-BiJL30sl.js +1 -0
- package/dist/client/assets/prod-qAk7rHa4.js +177 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-Drrw14YH.js → quadrantDiagram-AXDQQJYC-DwexhEhd.js} +1 -1
- package/dist/client/assets/quick-switcher-controller-BoIK_wuG.js +1 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BVA5UbMu.js +1 -0
- package/dist/client/assets/{requirementDiagram-EFPCY7ZU-CRZG6mRB.js → requirementDiagram-BXWQKSXE-C4cKwiIw.js} +2 -2
- package/dist/client/assets/runtime-config-DcmnGcXU.js +1 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-DvarEJpo.js → sankeyDiagram-P5KCCOFB-B5HHkRT2.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-B4oWlb-w.js → sequenceDiagram-WJ2MYXX4-BX6jqJFD.js} +6 -6
- package/dist/client/assets/{stateDiagram-HBIQ2CUA-qbHIIqRZ.js → stateDiagram-D77RDMKH-CmqxyRGx.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-zoLLQVyh.js +1 -0
- package/dist/client/assets/{swimlanes-XN3QIQJK-xB6CtUGj.js → swimlanes-42K2YHIH-Bmgb9Zld.js} +1 -1
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DS-KQN37.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-Lj_yTNlY.js → timeline-definition-24CTP7MA-BA-lVH6P.js} +1 -1
- package/dist/client/assets/vault-api-client-iZLhMJKe.js +6 -0
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BekRp5Y0.js → vennDiagram-4TSXK5OY-DNdUC_Xj.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-kdnV_Hdm.js → wardleyDiagram-VM6X3IG4-C2oY-YbF.js} +1 -1
- package/dist/client/assets/wiki-link-resolver-DFIwErjc.js +1 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-DufGwNSB.js → xychartDiagram-S5SC5T6Z-CoIR8CBX.js} +1 -1
- package/dist/client/assets/{yjs-provider-reset-guard-DE0vINDP.js → yjs-provider-reset-guard-9ul1qF6k.js} +1 -1
- package/dist/client/drawio-editor.html +11 -3
- package/dist/client/excalidraw-editor.html +45 -2
- package/dist/client/export-document.html +3 -3
- package/dist/client/index.html +67 -35
- package/integrations/pi/index.ts +94 -0
- package/package.json +41 -32
- package/src/client/app/drawio-editor.html +2 -2
- package/src/client/app/excalidraw-editor.html +2 -2
- package/src/client/app/export-document.html +1 -1
- package/src/client/app/index.html +49 -33
- package/src/client/app/main-entry.js +15 -1
- package/src/client/application/app-shell/chat-feature.js +7 -22
- package/src/client/application/app-shell/git-feature.js +23 -5
- package/src/client/application/app-shell/presence-feature.js +8 -25
- package/src/client/application/app-shell/ui-feature-shell.js +36 -91
- package/src/client/application/app-shell/ui-feature-sidebar.js +43 -7
- package/src/client/application/app-shell/ui-feature-tab-activity.js +19 -6
- package/src/client/application/app-shell/ui-feature-toolbar.js +16 -129
- package/src/client/application/app-shell-elements.js +4 -0
- package/src/client/application/preview-render-compiler.js +1 -87
- package/src/client/application/preview-render-scheduler.js +8 -24
- package/src/client/application/preview-renderer.js +2 -2
- package/src/client/application/webmcp-diagram-renderer.js +47 -0
- package/src/client/application/workspace-coordinator.js +3 -1
- package/src/client/application/workspace-route-controller.js +7 -0
- package/src/client/bootstrap/collabmd-app-shell.js +78 -1
- package/src/client/domain/excalidraw-element-link.js +73 -0
- package/src/client/domain/excalidraw-scene.js +21 -0
- package/src/client/domain/hash-routes.js +8 -0
- package/src/client/domain/room.js +1 -5
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/excalidraw-editor.js +135 -10
- package/src/client/export/export-host.js +3 -1
- package/src/client/export/export-pipeline.js +4 -17
- package/src/client/infrastructure/agent-connection-api-client.js +23 -0
- package/src/client/infrastructure/auth-client.js +3 -13
- package/src/client/infrastructure/editor-session.js +11 -0
- package/src/client/infrastructure/editor-view-adapter.js +13 -0
- package/src/client/infrastructure/excalidraw-room-client.js +80 -1
- package/src/client/infrastructure/git-api-client.js +39 -33
- package/src/client/infrastructure/lobby-presence.js +2 -5
- package/src/client/infrastructure/runtime-config.js +11 -1
- package/src/client/infrastructure/tab-activity-lock.js +2 -5
- package/src/client/infrastructure/vault-api-client.js +33 -26
- package/src/client/infrastructure/webmcp-tool-registry.js +283 -0
- package/src/client/infrastructure/workspace-sync-client.js +6 -4
- package/src/client/presentation/agent-connection-controller.js +255 -0
- package/src/client/presentation/backlinks-panel.js +3 -0
- package/src/client/presentation/bases-preview-controller.js +27 -18
- package/src/client/presentation/comment-ui/comment-ui-card.js +4 -2
- package/src/client/presentation/create-menu-presenter.js +19 -16
- package/src/client/presentation/excalidraw-embed-controller.js +164 -4
- package/src/client/presentation/file-explorer-view.js +70 -12
- package/src/client/presentation/file-history-view-controller.js +13 -11
- package/src/client/presentation/git-diff-view-controller.js +35 -43
- package/src/client/presentation/git-panel-controller.js +77 -27
- package/src/client/presentation/image-lightbox-controller.js +20 -16
- package/src/client/presentation/layout-controller.js +3 -1
- package/src/client/presentation/quick-switcher-controller.js +20 -78
- package/src/client/presentation/quick-switcher-text-search.js +10 -14
- package/src/client/styles/components/navigation.css +1 -2
- package/src/client/styles/features/agent-connection.css +202 -0
- package/src/client/styles/features/collaboration-chat.css +8 -1
- package/src/client/styles/features/collaboration-dialogs.css +16 -1
- package/src/client/styles/features/collaboration-presence.css +17 -4
- package/src/client/styles/features/diff-toolbar.css +1 -1
- package/src/client/styles/features/media-lightbox.css +12 -3
- package/src/client/styles/features/quick-switcher.css +20 -11
- package/src/client/styles/foundation/themes.css +2 -0
- package/src/client/styles/layout/editor-page.css +25 -11
- package/src/client/styles/layout/responsive.css +2 -20
- package/src/client/styles/layout/shell.css +1 -0
- package/src/client/styles/layout/sidebar.css +7 -4
- package/src/client/styles/primitives/controls.css +21 -54
- package/src/client/styles/style.css +1 -0
- package/src/domain/agent-tool-definitions.js +796 -0
- package/src/domain/collabmd-content-capabilities.js +149 -0
- package/src/domain/collaboration-protocol.js +2 -0
- package/src/domain/comment-threads.js +3 -5
- package/src/domain/diagram-source.js +50 -0
- package/src/domain/editable-content-revision.js +10 -0
- package/src/domain/editable-text.js +3 -0
- package/src/domain/exact-text-edits.js +69 -0
- package/src/domain/excalidraw-agent-scene.js +522 -0
- package/src/domain/excalidraw-agent-verification.js +460 -0
- package/src/domain/excalidraw-room-codec.js +1 -5
- package/src/domain/video-embed.js +76 -0
- package/src/server/application/agent-connection-service.js +178 -0
- package/src/server/application/agent-content-service.js +773 -0
- package/src/server/application/workspace-reconciliation.js +5 -7
- package/src/server/config/env.js +51 -1
- package/src/server/create-app-server.js +36 -0
- package/src/server/domain/bases/base-query-service.js +1 -5
- package/src/server/domain/collaboration/collaboration-room.js +121 -25
- package/src/server/domain/collaboration/protocol.js +4 -0
- package/src/server/domain/hosted-workspace.js +21 -0
- package/src/server/domain/markdown-reference-extractor.js +15 -1
- package/src/server/infrastructure/git/git-service.js +19 -1
- package/src/server/infrastructure/git/history-service.js +1 -1
- package/src/server/infrastructure/git/local-exclude.js +25 -0
- package/src/server/infrastructure/git/parsers.js +19 -17
- package/src/server/infrastructure/git/path-utils.js +0 -16
- package/src/server/infrastructure/git/status-service.js +1 -1
- package/src/server/infrastructure/git/untracked-files.js +12 -21
- package/src/server/infrastructure/http/create-agent-api-handler.js +171 -0
- package/src/server/infrastructure/http/create-git-api-command-handler.js +32 -0
- package/src/server/infrastructure/http/create-request-handler.js +28 -0
- package/src/server/infrastructure/http/create-static-handler.js +6 -0
- package/src/server/infrastructure/mcp/create-agent-mcp-handler.js +199 -0
- package/src/server/infrastructure/persistence/agent-connection-store.js +120 -0
- package/src/server/infrastructure/persistence/pull-backup-store.js +2 -27
- package/src/server/infrastructure/persistence/vault-file-store.js +9 -2
- package/src/server/shared/agent-tool-image.js +22 -0
- package/src/server/shared/agent-tool-rate-limiter.js +20 -0
- package/src/server/startup/git-remote-bootstrap.js +5 -33
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-BPH4FJkv.js +0 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-BNDLzJ7L.js +0 -1
- package/dist/client/assets/browser-utils--H_DkyEF.js +0 -1
- package/dist/client/assets/channel-CaoKA_We.js +0 -1
- package/dist/client/assets/chunk-DSNCTWBM-8xkug7fQ.js +0 -2
- package/dist/client/assets/chunk-DU6HZSFF-COXDzJK1.js +0 -127
- package/dist/client/assets/chunk-R7TYR2AO-CTd0Yj98.js +0 -10
- package/dist/client/assets/classDiagram-DTDB5LWJ-7fEmn0zV.js +0 -1
- package/dist/client/assets/classDiagram-v2-JRS7N3AN-7fEmn0zV.js +0 -1
- package/dist/client/assets/comment-threads-9J6Em-_y.js +0 -4
- package/dist/client/assets/comment-ui-shared-DuoyznXW.js +0 -7
- package/dist/client/assets/diagram-VSXAHHWV-CQRVEZR7.js +0 -3
- package/dist/client/assets/dist-73RPdcyU.js +0 -1
- package/dist/client/assets/dist-CH-sQu3r.js +0 -1
- package/dist/client/assets/document-formatter-Dxl_a8XO.js +0 -2
- package/dist/client/assets/drawio-editor-BVaNNtYp.js +0 -9
- package/dist/client/assets/drawioEditor-BkIBlXx0.js +0 -2
- package/dist/client/assets/ebnfDiagram-PWID7BFC-OXa8VEp1.js +0 -1
- package/dist/client/assets/editor-session-DMyVdHLj.js +0 -54
- package/dist/client/assets/excalidraw-editor-BjYaNEMY.js +0 -9
- package/dist/client/assets/excalidraw-mermaid-stub-BNckbX2n.js +0 -1
- package/dist/client/assets/excalidraw-mermaid-stub-DE5FTiZB.js +0 -1
- package/dist/client/assets/excalidrawEditor-ChqF_d82.js +0 -2
- package/dist/client/assets/export-page-Dyu0XCbu.js +0 -57
- package/dist/client/assets/exportDocument-CJBx4Xjx.css +0 -1
- package/dist/client/assets/exportDocument-Dgvfa2Sd.js +0 -2
- package/dist/client/assets/flowDiagram-A5DVABFB-CiW1eN8C.js +0 -1
- package/dist/client/assets/image-ZVKINSPA-0zXSnE7M.js +0 -1
- package/dist/client/assets/index-CSNIMyOP.css +0 -1
- package/dist/client/assets/index-DG3N2Ps-.js +0 -2
- package/dist/client/assets/main-D0wCxpVn.js +0 -903
- package/dist/client/assets/mermaid.core-BJRLUQZE.js +0 -11
- package/dist/client/assets/pegDiagram-XKGWAZYB-B31fKPR2.js +0 -1
- package/dist/client/assets/preview-render-compiler-CrJIooVD.js +0 -35
- package/dist/client/assets/preview-render-compiler-O0JxBVNF.js +0 -1
- package/dist/client/assets/preview-render-profile-B4RWYWHt.js +0 -1
- package/dist/client/assets/prod-BiYPMR1i.js +0 -177
- package/dist/client/assets/prod-VxdGZ5Ly.js +0 -1
- package/dist/client/assets/quick-switcher-controller-CaEjEtq_.js +0 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BPifP0MB.js +0 -1
- package/dist/client/assets/runtime-config-BLrEKVO1.js +0 -1
- package/dist/client/assets/stateDiagram-v2-4QOOHH4V-B--gjrit.js +0 -1
- package/dist/client/assets/swimlanesDiagram-VK2B7HYN-DO8XNXEg.js +0 -8
- package/dist/client/assets/vault-api-client-BkwdFjx0.js +0 -6
- package/src/client/app/drawio-editor-entry.js +0 -1
- package/src/client/app/excalidraw-editor-entry.js +0 -1
- package/src/client/excalidraw-mermaid-stub.js +0 -7
- package/src/client/main.js +0 -16
- /package/dist/client/assets/{drawio-editor-UQQ8XHou.css → drawioEditor-UQQ8XHou.css} +0 -0
- /package/dist/client/assets/{excalidraw-editor-BbSbI80a.css → excalidrawEditor-BbSbI80a.css} +0 -0
- /package/dist/client/assets/{sizeCapture-X5ZJPWSS-B0uUizjq.js → sizeCapture-INFHLROL-B0uUizjq.js} +0 -0
|
@@ -34,22 +34,6 @@ export function normalizeRelativeGitPath(pathValue) {
|
|
|
34
34
|
return normalized.join('/');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function parseRenamePath(rawPath) {
|
|
38
|
-
const decodedPath = decodeQuotedPath(rawPath);
|
|
39
|
-
const separator = ' -> ';
|
|
40
|
-
const separatorIndex = decodedPath.indexOf(separator);
|
|
41
|
-
if (separatorIndex === -1) {
|
|
42
|
-
return {
|
|
43
|
-
oldPath: null,
|
|
44
|
-
path: decodedPath,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
oldPath: decodedPath.slice(0, separatorIndex),
|
|
50
|
-
path: decodedPath.slice(separatorIndex + separator.length),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
37
|
|
|
54
38
|
export function stripDiffPrefix(pathValue) {
|
|
55
39
|
const normalizedPath = String(pathValue ?? '').replace(/\s+$/u, '');
|
|
@@ -42,7 +42,7 @@ export class GitStatusService {
|
|
|
42
42
|
|
|
43
43
|
const statusPromise = (async () => {
|
|
44
44
|
const parsed = parseStatusOutput(
|
|
45
|
-
await this.commandRunner.execGit(['status', '--porcelain=v1', '--branch', '--untracked-files=all']),
|
|
45
|
+
await this.commandRunner.execGit(['status', '--porcelain=v1', '-z', '--branch', '--untracked-files=all']),
|
|
46
46
|
);
|
|
47
47
|
const sections = [
|
|
48
48
|
{ files: parsed.sections.staged, key: 'staged', label: 'Staged Changes' },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
|
|
5
6
|
import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
|
|
6
7
|
import { mapWithConcurrency } from '../../shared/async-utils.js';
|
|
@@ -13,29 +14,19 @@ async function countFileLines(filePath) {
|
|
|
13
14
|
return 0;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const lines = createInterface({
|
|
18
|
+
crlfDelay: Infinity,
|
|
19
|
+
input: createReadStream(filePath, { encoding: 'utf8' }),
|
|
20
|
+
});
|
|
21
|
+
let lineCount = 0;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
for await (const line of lines) {
|
|
24
|
+
if (typeof line === 'string') {
|
|
25
|
+
lineCount += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
lastCharacter = chunk[chunk.length - 1];
|
|
29
|
-
lineCount += chunk.match(/\n/gu)?.length ?? 0;
|
|
30
|
-
});
|
|
31
|
-
stream.on('error', reject);
|
|
32
|
-
stream.on('end', () => {
|
|
33
|
-
if (sawData && lastCharacter !== '\n') {
|
|
34
|
-
lineCount += 1;
|
|
35
|
-
}
|
|
36
|
-
resolve(lineCount);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
29
|
+
return lineCount;
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
function buildSyntheticAddedFileDiff(pathValue, content) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { fromJsonSchema } from '@modelcontextprotocol/server';
|
|
2
|
+
|
|
3
|
+
import { getAgentToolDefinition } from '../../../domain/agent-tool-definitions.js';
|
|
4
|
+
import { encodeAgentToolImage } from '../../shared/agent-tool-image.js';
|
|
5
|
+
import { readRequestId } from './http-request-helpers.js';
|
|
6
|
+
import { jsonResponse } from './http-response.js';
|
|
7
|
+
import { parseJsonBody } from './request-body.js';
|
|
8
|
+
|
|
9
|
+
const WEBMCP_TOOL_PATH = '/api/agent/tools/';
|
|
10
|
+
const WEBMCP_INPUT_SCHEMAS = new Map();
|
|
11
|
+
|
|
12
|
+
function createAgentError(code, message, statusCode) {
|
|
13
|
+
const error = new Error(message);
|
|
14
|
+
error.code = code;
|
|
15
|
+
error.statusCode = statusCode;
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sendAgentError(req, res, error) {
|
|
20
|
+
const statusCode = Number(error?.statusCode) || 500;
|
|
21
|
+
if (statusCode >= 500) console.error('[agent] Request failed:', error.message);
|
|
22
|
+
jsonResponse(req, res, statusCode, {
|
|
23
|
+
code: error?.code || 'AGENT_REQUEST_FAILED',
|
|
24
|
+
error: statusCode >= 500 ? 'Agent request failed' : error.message,
|
|
25
|
+
...(Number.isFinite(error?.retryAfterMs) ? { retryAfterMs: error.retryAfterMs } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function validateWebMcpInput(definition, input) {
|
|
30
|
+
let schema = WEBMCP_INPUT_SCHEMAS.get(definition.name);
|
|
31
|
+
if (!schema) {
|
|
32
|
+
schema = fromJsonSchema(definition.inputSchema);
|
|
33
|
+
WEBMCP_INPUT_SCHEMAS.set(definition.name, schema);
|
|
34
|
+
}
|
|
35
|
+
const validation = await schema['~standard'].validate(input);
|
|
36
|
+
if (validation.issues?.length) {
|
|
37
|
+
throw createAgentError('AGENT_INPUT_INVALID', 'WebMCP tool input does not match its schema', 400);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createWebMcpActor({ agentConnectionService, authService, config, req }) {
|
|
42
|
+
let subject;
|
|
43
|
+
if (config.auth.strategy === 'none') {
|
|
44
|
+
subject = {
|
|
45
|
+
collaborator: null,
|
|
46
|
+
subjectId: req.socket?.remoteAddress || 'unknown',
|
|
47
|
+
subjectType: 'anonymous',
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
const user = authService.getAuthenticatedUser?.(req) ?? null;
|
|
51
|
+
subject = await agentConnectionService.resolveManagementSubject(user);
|
|
52
|
+
}
|
|
53
|
+
const actorId = `webmcp:${subject.subjectType}:${subject.subjectId}`;
|
|
54
|
+
return {
|
|
55
|
+
collaborator: subject.collaborator,
|
|
56
|
+
connectionId: actorId,
|
|
57
|
+
origin: 'webmcp',
|
|
58
|
+
rateLimitKey: actorId,
|
|
59
|
+
requestId: readRequestId(req),
|
|
60
|
+
scopes: ['vault:edit', 'vault:read'],
|
|
61
|
+
sourceRef: 'webmcp',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function mapWebMcpResult(definition, value) {
|
|
66
|
+
if (definition.resultKind === 'image' && value.svg) {
|
|
67
|
+
const { data, mimeType, structuredContent } = await encodeAgentToolImage(value);
|
|
68
|
+
return {
|
|
69
|
+
...structuredContent,
|
|
70
|
+
image: { data, encoding: 'base64', mimeType },
|
|
71
|
+
scene: value.scene,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (definition.resultKind === 'optional-image' && value.verification?.svg) {
|
|
75
|
+
const { verification, ...result } = value;
|
|
76
|
+
const { data, mimeType, structuredContent } = await encodeAgentToolImage(verification);
|
|
77
|
+
return {
|
|
78
|
+
...result,
|
|
79
|
+
image: { data, encoding: 'base64', mimeType },
|
|
80
|
+
verification: {
|
|
81
|
+
...structuredContent,
|
|
82
|
+
scene: verification.scene,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createAgentApiHandler({
|
|
90
|
+
agentConnectionService,
|
|
91
|
+
agentContentService,
|
|
92
|
+
authService,
|
|
93
|
+
config,
|
|
94
|
+
rateLimiter,
|
|
95
|
+
}) {
|
|
96
|
+
return async function handleAgentApi(req, res, requestUrl) {
|
|
97
|
+
if (!requestUrl.pathname.startsWith('/api/agent')) return false;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (requestUrl.pathname.startsWith(WEBMCP_TOOL_PATH)) {
|
|
101
|
+
if (req.method !== 'POST') {
|
|
102
|
+
throw createAgentError('AGENT_METHOD_NOT_ALLOWED', 'WebMCP tools require POST', 405);
|
|
103
|
+
}
|
|
104
|
+
const name = decodeURIComponent(requestUrl.pathname.slice(WEBMCP_TOOL_PATH.length));
|
|
105
|
+
const definition = getAgentToolDefinition(name);
|
|
106
|
+
if (!definition?.webMcp) {
|
|
107
|
+
throw createAgentError('AGENT_TOOL_NOT_FOUND', 'WebMCP tool not found', 404);
|
|
108
|
+
}
|
|
109
|
+
const input = await parseJsonBody(req);
|
|
110
|
+
await validateWebMcpInput(definition, input);
|
|
111
|
+
const actor = await createWebMcpActor({
|
|
112
|
+
agentConnectionService,
|
|
113
|
+
authService,
|
|
114
|
+
config,
|
|
115
|
+
req,
|
|
116
|
+
});
|
|
117
|
+
rateLimiter(actor.rateLimitKey);
|
|
118
|
+
const cancellation = new AbortController();
|
|
119
|
+
const abort = () => cancellation.abort();
|
|
120
|
+
req.once('aborted', abort);
|
|
121
|
+
try {
|
|
122
|
+
const value = await agentContentService[definition.method](actor, {
|
|
123
|
+
...input,
|
|
124
|
+
signal: cancellation.signal,
|
|
125
|
+
});
|
|
126
|
+
jsonResponse(req, res, 200, await mapWebMcpResult(definition, value));
|
|
127
|
+
} finally {
|
|
128
|
+
req.off('aborted', abort);
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!config.agentAccess?.enabled) {
|
|
134
|
+
jsonResponse(req, res, 404, { error: 'Agent Access is not enabled' });
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const user = authService.getAuthenticatedUser?.(req) ?? null;
|
|
138
|
+
if (requestUrl.pathname === '/api/agent/connections' && req.method === 'GET') {
|
|
139
|
+
jsonResponse(req, res, 200, {
|
|
140
|
+
connections: await agentConnectionService.listConnections(user),
|
|
141
|
+
});
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (requestUrl.pathname === '/api/agent/connections' && req.method === 'POST') {
|
|
145
|
+
const body = await parseJsonBody(req);
|
|
146
|
+
jsonResponse(req, res, 201, await agentConnectionService.createConnection({
|
|
147
|
+
clientKind: body?.clientKind,
|
|
148
|
+
label: body?.label,
|
|
149
|
+
scopes: body?.scopes,
|
|
150
|
+
user,
|
|
151
|
+
}));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (requestUrl.pathname.startsWith('/api/agent/connections/') && req.method === 'DELETE') {
|
|
155
|
+
const connectionId = decodeURIComponent(
|
|
156
|
+
requestUrl.pathname.slice('/api/agent/connections/'.length).split('/')[0] || '',
|
|
157
|
+
);
|
|
158
|
+
jsonResponse(req, res, 200, await agentConnectionService.revokeConnection({
|
|
159
|
+
connectionId,
|
|
160
|
+
user,
|
|
161
|
+
}));
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
jsonResponse(req, res, 404, { error: 'Agent endpoint not found' });
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
sendAgentError(req, res, error);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -84,6 +84,38 @@ export function createGitApiCommandHandler({
|
|
|
84
84
|
return true;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
if (requestUrl.pathname === '/api/git/stage-all' && req.method === 'POST') {
|
|
88
|
+
try {
|
|
89
|
+
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
90
|
+
action: 'stage-all',
|
|
91
|
+
req,
|
|
92
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
93
|
+
() => gitService.stageAll(),
|
|
94
|
+
),
|
|
95
|
+
workspaceMutationCoordinator,
|
|
96
|
+
}));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
handleApiError(req, res, error, '[api] Failed to stage all git changes:', 'Failed to stage all git changes');
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (requestUrl.pathname === '/api/git/unstage-all' && req.method === 'POST') {
|
|
104
|
+
try {
|
|
105
|
+
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
106
|
+
action: 'unstage-all',
|
|
107
|
+
req,
|
|
108
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
109
|
+
() => gitService.unstageAll(),
|
|
110
|
+
),
|
|
111
|
+
workspaceMutationCoordinator,
|
|
112
|
+
}));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
handleApiError(req, res, error, '[api] Failed to unstage all git changes:', 'Failed to unstage all git changes');
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
if (requestUrl.pathname === '/api/git/commit' && req.method === 'POST') {
|
|
88
120
|
try {
|
|
89
121
|
const body = await parseRequiredBody(req, res, 'message');
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createAgentApiHandler } from './create-agent-api-handler.js';
|
|
2
|
+
import { createAgentMcpHandler } from '../mcp/create-agent-mcp-handler.js';
|
|
3
|
+
import { createAgentToolRateLimiter } from '../../shared/agent-tool-rate-limiter.js';
|
|
1
4
|
import { createAuthApiHandler } from './create-auth-api-handler.js';
|
|
2
5
|
import { createGitApiCommandHandler } from './create-git-api-command-handler.js';
|
|
3
6
|
import { createGitApiQueryHandler } from './create-git-api-query-handler.js';
|
|
@@ -38,6 +41,7 @@ export function createRequestHandler(
|
|
|
38
41
|
hostedWorkspaceService = null,
|
|
39
42
|
githubSetupFlow = null,
|
|
40
43
|
structurizrWorkspaceService = null,
|
|
44
|
+
agentIntegration = null,
|
|
41
45
|
) {
|
|
42
46
|
const handleStaticRequest = createStaticHandler(config, authService, searchService);
|
|
43
47
|
const handleAuthApi = createAuthApiHandler({ authService });
|
|
@@ -73,6 +77,22 @@ export function createRequestHandler(
|
|
|
73
77
|
basePath: config.basePath,
|
|
74
78
|
service: structurizrWorkspaceService,
|
|
75
79
|
});
|
|
80
|
+
const agentToolRateLimiter = createAgentToolRateLimiter(config.agentAccess.requestsPerMinute);
|
|
81
|
+
const handleAgentApi = createAgentApiHandler({
|
|
82
|
+
agentConnectionService: agentIntegration?.connectionService,
|
|
83
|
+
agentContentService: agentIntegration?.contentService,
|
|
84
|
+
authService,
|
|
85
|
+
config,
|
|
86
|
+
rateLimiter: agentToolRateLimiter,
|
|
87
|
+
});
|
|
88
|
+
const handleAgentMcp = config.agentAccess.enabled && agentIntegration
|
|
89
|
+
? createAgentMcpHandler({
|
|
90
|
+
agentConnectionService: agentIntegration.connectionService,
|
|
91
|
+
agentContentService: agentIntegration.contentService,
|
|
92
|
+
config,
|
|
93
|
+
rateLimiter: agentToolRateLimiter,
|
|
94
|
+
})
|
|
95
|
+
: async () => false;
|
|
76
96
|
|
|
77
97
|
function handleBasePathRedirect(req, res, originalRequestUrl) {
|
|
78
98
|
if (
|
|
@@ -182,6 +202,10 @@ export function createRequestHandler(
|
|
|
182
202
|
return;
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
if (await handleAgentMcp(req, res, requestUrl)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
if (requestUrl.pathname.startsWith('/api/') || handleStructurizrApi.requiresAuthorization(requestUrl)) {
|
|
186
210
|
const authorization = authService.authorizeApiRequest(req);
|
|
187
211
|
if (!authorization.ok) {
|
|
@@ -198,6 +222,10 @@ export function createRequestHandler(
|
|
|
198
222
|
}
|
|
199
223
|
}
|
|
200
224
|
|
|
225
|
+
if (await handleAgentApi(req, res, requestUrl)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
201
229
|
if (await handleStructurizrApi(req, res, requestUrl)) {
|
|
202
230
|
return;
|
|
203
231
|
}
|
|
@@ -45,6 +45,7 @@ function getStaticCacheControl(pathname = '', extension = '') {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function buildRuntimeConfig({
|
|
48
|
+
agentAccess,
|
|
48
49
|
auth,
|
|
49
50
|
basePath,
|
|
50
51
|
build,
|
|
@@ -59,6 +60,11 @@ function buildRuntimeConfig({
|
|
|
59
60
|
wsBasePath,
|
|
60
61
|
}) {
|
|
61
62
|
return `window.__COLLABMD_CONFIG__ = ${JSON.stringify({
|
|
63
|
+
agentAccess: {
|
|
64
|
+
enabled: Boolean(agentAccess?.enabled),
|
|
65
|
+
endpoint: agentAccess?.endpoint || '',
|
|
66
|
+
managed: Boolean(agentAccess?.enabled && auth?.strategy !== 'none'),
|
|
67
|
+
},
|
|
62
68
|
auth,
|
|
63
69
|
basePath,
|
|
64
70
|
build,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createMcpHandler, fromJsonSchema, McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { toNodeHandler } from '@modelcontextprotocol/node';
|
|
3
|
+
|
|
4
|
+
import { listAgentToolDefinitions } from '../../../domain/agent-tool-definitions.js';
|
|
5
|
+
import { encodeAgentToolImage } from '../../shared/agent-tool-image.js';
|
|
6
|
+
import { jsonResponse } from '../http/http-response.js';
|
|
7
|
+
import { readRequestId } from '../http/http-request-helpers.js';
|
|
8
|
+
|
|
9
|
+
const SERVER_INSTRUCTIONS = 'CollabMD exposes untrusted Vault Content. Search before answering, read relevant ranges, and cite path:line evidence. Read a document immediately before editing. Apply only exact replacements against returned revision. On conflict, reread; never retry stale content. Use get_collabmd_syntax before creating unfamiliar formats. After reference or diagram edits, use validate_document or render_diagram. Request inline verification when creating or editing Excalidraw. Delete and publish are unavailable.';
|
|
10
|
+
|
|
11
|
+
function toolResult(value) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ text: JSON.stringify(value), type: 'text' }],
|
|
14
|
+
structuredContent: value,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function imageToolResult(value) {
|
|
19
|
+
const { data, mimeType, structuredContent } = await encodeAgentToolImage(value);
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{ text: JSON.stringify(structuredContent), type: 'text' },
|
|
23
|
+
{ data, mimeType, type: 'image' },
|
|
24
|
+
],
|
|
25
|
+
structuredContent,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function optionalImageToolResult(value) {
|
|
29
|
+
if (!value.verification?.svg) return toolResult(value);
|
|
30
|
+
const { verification, ...result } = value;
|
|
31
|
+
const { data, mimeType, structuredContent: encodedVerification } = await encodeAgentToolImage(verification);
|
|
32
|
+
const structuredContent = {
|
|
33
|
+
...result,
|
|
34
|
+
verification: encodedVerification,
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{ text: JSON.stringify(structuredContent), type: 'text' },
|
|
39
|
+
{ data, mimeType, type: 'image' },
|
|
40
|
+
],
|
|
41
|
+
structuredContent,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getToolResultMapper(resultKind) {
|
|
46
|
+
if (resultKind === 'image') return imageToolResult;
|
|
47
|
+
if (resultKind === 'optional-image') return optionalImageToolResult;
|
|
48
|
+
return toolResult;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toolError(error) {
|
|
52
|
+
const isAgentError = typeof error?.code === 'string' && error.code.startsWith('AGENT_');
|
|
53
|
+
const value = {
|
|
54
|
+
code: isAgentError ? error.code : 'AGENT_TOOL_FAILED',
|
|
55
|
+
error: isAgentError ? error.message : 'Agent tool failed',
|
|
56
|
+
};
|
|
57
|
+
if (isAgentError && Number.isFinite(error?.retryAfterMs)) {
|
|
58
|
+
value.retryAfterMs = error.retryAfterMs;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
content: [{ text: JSON.stringify(value), type: 'text' }],
|
|
62
|
+
isError: true,
|
|
63
|
+
structuredContent: value,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function registerTool(server, name, config, handler, {
|
|
68
|
+
actor,
|
|
69
|
+
rateLimiter,
|
|
70
|
+
resultMapper = toolResult,
|
|
71
|
+
}) {
|
|
72
|
+
server.registerTool(name, config, async (input, context) => {
|
|
73
|
+
try {
|
|
74
|
+
rateLimiter(actor.rateLimitKey);
|
|
75
|
+
return await resultMapper(await handler(input, context));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (typeof error?.code !== 'string' || !error.code.startsWith('AGENT_')) {
|
|
78
|
+
console.error(`[mcp] ${name} failed (${actor.requestId}):`, error?.message || 'Unknown error');
|
|
79
|
+
}
|
|
80
|
+
return toolError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildAgentServer({ actor, agentContentService, rateLimiter, version }) {
|
|
86
|
+
const server = new McpServer(
|
|
87
|
+
{ name: 'collabmd', version },
|
|
88
|
+
{ instructions: SERVER_INSTRUCTIONS },
|
|
89
|
+
);
|
|
90
|
+
for (const definition of listAgentToolDefinitions(actor.scopes)) {
|
|
91
|
+
registerTool(server, definition.name, {
|
|
92
|
+
annotations: definition.annotations,
|
|
93
|
+
description: definition.description,
|
|
94
|
+
inputSchema: fromJsonSchema(definition.inputSchema),
|
|
95
|
+
outputSchema: fromJsonSchema(definition.outputSchema),
|
|
96
|
+
}, (input, context) => agentContentService[definition.method](actor, {
|
|
97
|
+
...input,
|
|
98
|
+
signal: context.signal,
|
|
99
|
+
}), {
|
|
100
|
+
actor,
|
|
101
|
+
rateLimiter,
|
|
102
|
+
resultMapper: getToolResultMapper(definition.resultKind),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return server;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readHttpHostname(value) {
|
|
109
|
+
try {
|
|
110
|
+
const url = new URL(value);
|
|
111
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return '';
|
|
112
|
+
return url.hostname.replace(/^\[|\]$/gu, '').toLowerCase();
|
|
113
|
+
} catch {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readRequestHostname(req) {
|
|
119
|
+
return readHttpHostname(`http://${String(req.headers.host || '')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createAgentMcpHandler({
|
|
123
|
+
agentConnectionService,
|
|
124
|
+
agentContentService,
|
|
125
|
+
config,
|
|
126
|
+
rateLimiter,
|
|
127
|
+
}) {
|
|
128
|
+
const mcpHandler = createMcpHandler(({ authInfo }) => buildAgentServer({
|
|
129
|
+
actor: authInfo.extra.actor,
|
|
130
|
+
agentContentService,
|
|
131
|
+
rateLimiter,
|
|
132
|
+
version: config.build?.version || '0.1.0',
|
|
133
|
+
}));
|
|
134
|
+
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
135
|
+
onerror: (error) => console.error('[mcp] Transport failed:', error.message),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return async function handleAgentMcp(req, res, requestUrl) {
|
|
139
|
+
if (requestUrl.pathname !== '/mcp') return false;
|
|
140
|
+
const authRequired = config.auth.strategy !== 'none';
|
|
141
|
+
const allowedHosts = config.agentAccess.allowedHosts;
|
|
142
|
+
const hostname = readRequestHostname(req);
|
|
143
|
+
const hasOrigin = Boolean(req.headers.origin);
|
|
144
|
+
const originHostname = hasOrigin ? readHttpHostname(String(req.headers.origin)) : '';
|
|
145
|
+
const browserOriginAllowed = hasOrigin
|
|
146
|
+
&& hostname
|
|
147
|
+
&& originHostname === hostname
|
|
148
|
+
&& allowedHosts.includes(hostname);
|
|
149
|
+
if (
|
|
150
|
+
(authRequired && (!hostname || !allowedHosts.includes(hostname)))
|
|
151
|
+
|| (hasOrigin && !browserOriginAllowed)
|
|
152
|
+
) {
|
|
153
|
+
jsonResponse(req, res, 403, { error: 'MCP request origin or host is not allowed' });
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let actor;
|
|
158
|
+
let token = '';
|
|
159
|
+
if (authRequired) {
|
|
160
|
+
const authorization = String(req.headers.authorization || '');
|
|
161
|
+
const match = authorization.match(/^Bearer\s+(.+)$/iu);
|
|
162
|
+
if (!match) {
|
|
163
|
+
res.setHeader('WWW-Authenticate', 'Bearer scope="vault:read"');
|
|
164
|
+
jsonResponse(req, res, 401, { error: 'Agent Connection token is required' });
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
token = match[1];
|
|
168
|
+
try {
|
|
169
|
+
actor = await agentConnectionService.authenticateToken(token);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
res.setHeader('WWW-Authenticate', 'Bearer scope="vault:read"');
|
|
172
|
+
jsonResponse(req, res, Number(error?.statusCode) || 401, {
|
|
173
|
+
code: error?.code || 'AGENT_TOKEN_INVALID',
|
|
174
|
+
error: error?.message || 'Agent Connection token is invalid',
|
|
175
|
+
});
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
actor = {
|
|
180
|
+
collaborator: null,
|
|
181
|
+
connectionId: 'anonymous',
|
|
182
|
+
scopes: ['vault:edit', 'vault:read'],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
actor.requestId = readRequestId(req);
|
|
187
|
+
actor.rateLimitKey = actor.connectionId === 'anonymous'
|
|
188
|
+
? `anonymous:${req.socket?.remoteAddress || 'unknown'}`
|
|
189
|
+
: actor.connectionId;
|
|
190
|
+
req.auth = {
|
|
191
|
+
clientId: actor.connectionId,
|
|
192
|
+
extra: { actor },
|
|
193
|
+
scopes: actor.scopes,
|
|
194
|
+
token,
|
|
195
|
+
};
|
|
196
|
+
await nodeHandler(req, res);
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function parseScopes(value) {
|
|
5
|
+
try {
|
|
6
|
+
const scopes = JSON.parse(value || '[]');
|
|
7
|
+
return Array.isArray(scopes) ? scopes : [];
|
|
8
|
+
} catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function rowToConnection(row) {
|
|
14
|
+
if (!row) return null;
|
|
15
|
+
return {
|
|
16
|
+
clientKind: row.client_kind,
|
|
17
|
+
createdAt: row.created_at,
|
|
18
|
+
expiresAt: row.expires_at,
|
|
19
|
+
id: row.id,
|
|
20
|
+
label: row.label,
|
|
21
|
+
revokedAt: row.revoked_at,
|
|
22
|
+
scopes: parseScopes(row.scopes_json),
|
|
23
|
+
subjectEmail: row.subject_email,
|
|
24
|
+
subjectId: row.subject_id,
|
|
25
|
+
subjectType: row.subject_type,
|
|
26
|
+
tokenHash: row.token_hash,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class AgentConnectionStore {
|
|
31
|
+
constructor({ dbPath }) {
|
|
32
|
+
this.dbPath = dbPath;
|
|
33
|
+
this.db = null;
|
|
34
|
+
this.statements = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async initialize() {
|
|
38
|
+
if (this.db) return;
|
|
39
|
+
await mkdir(dirname(this.dbPath), { recursive: true });
|
|
40
|
+
const { DatabaseSync } = await import('node:sqlite');
|
|
41
|
+
this.db = new DatabaseSync(this.dbPath);
|
|
42
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
43
|
+
this.db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS agent_connections (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
subject_type TEXT NOT NULL,
|
|
47
|
+
subject_id TEXT NOT NULL,
|
|
48
|
+
subject_email TEXT NOT NULL DEFAULT '',
|
|
49
|
+
label TEXT NOT NULL,
|
|
50
|
+
client_kind TEXT NOT NULL,
|
|
51
|
+
scopes_json TEXT NOT NULL,
|
|
52
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
53
|
+
created_at INTEGER NOT NULL,
|
|
54
|
+
expires_at INTEGER NOT NULL,
|
|
55
|
+
revoked_at INTEGER
|
|
56
|
+
);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS agent_connections_subject_idx
|
|
58
|
+
ON agent_connections(subject_type, subject_id, created_at DESC);
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
prepare(key, sql) {
|
|
63
|
+
if (!this.statements.has(key)) this.statements.set(key, this.db.prepare(sql));
|
|
64
|
+
return this.statements.get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async close() {
|
|
68
|
+
this.statements.clear();
|
|
69
|
+
this.db?.close();
|
|
70
|
+
this.db = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async create(connection) {
|
|
74
|
+
this.prepare(
|
|
75
|
+
'create',
|
|
76
|
+
`INSERT INTO agent_connections (
|
|
77
|
+
id, subject_type, subject_id, subject_email, label, client_kind,
|
|
78
|
+
scopes_json, token_hash, created_at, expires_at, revoked_at
|
|
79
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
|
|
80
|
+
).run(
|
|
81
|
+
connection.id,
|
|
82
|
+
connection.subjectType,
|
|
83
|
+
connection.subjectId,
|
|
84
|
+
connection.subjectEmail,
|
|
85
|
+
connection.label,
|
|
86
|
+
connection.clientKind,
|
|
87
|
+
JSON.stringify(connection.scopes),
|
|
88
|
+
connection.tokenHash,
|
|
89
|
+
connection.createdAt,
|
|
90
|
+
connection.expiresAt,
|
|
91
|
+
);
|
|
92
|
+
return connection;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getByTokenHash(tokenHash) {
|
|
96
|
+
return rowToConnection(this.prepare(
|
|
97
|
+
'getByTokenHash',
|
|
98
|
+
'SELECT * FROM agent_connections WHERE token_hash = ? LIMIT 1',
|
|
99
|
+
).get(tokenHash));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async listBySubject(subjectType, subjectId) {
|
|
103
|
+
return this.prepare(
|
|
104
|
+
'listBySubject',
|
|
105
|
+
`SELECT * FROM agent_connections
|
|
106
|
+
WHERE subject_type = ? AND subject_id = ? AND revoked_at IS NULL
|
|
107
|
+
ORDER BY created_at DESC`,
|
|
108
|
+
).all(subjectType, subjectId).map(rowToConnection);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async revoke(id, subjectType, subjectId, revokedAt) {
|
|
112
|
+
const result = this.prepare(
|
|
113
|
+
'revoke',
|
|
114
|
+
`UPDATE agent_connections SET revoked_at = ?
|
|
115
|
+
WHERE id = ? AND subject_type = ? AND subject_id = ? AND revoked_at IS NULL`,
|
|
116
|
+
).run(revokedAt, id, subjectType, subjectId);
|
|
117
|
+
return Number(result.changes) > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
}
|