sidebar-md 0.1.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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/server/annotation-ops.d.ts +104 -0
- package/dist/server/annotation-ops.js +166 -0
- package/dist/server/annotation-ops.js.map +1 -0
- package/dist/server/args.d.ts +20 -0
- package/dist/server/args.js +118 -0
- package/dist/server/args.js.map +1 -0
- package/dist/server/author.d.ts +1 -0
- package/dist/server/author.js +45 -0
- package/dist/server/author.js.map +1 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +364 -0
- package/dist/server/cli.js.map +1 -0
- package/dist/server/config/gitignore.d.ts +10 -0
- package/dist/server/config/gitignore.js +53 -0
- package/dist/server/config/gitignore.js.map +1 -0
- package/dist/server/config/index.d.ts +5 -0
- package/dist/server/config/index.js +5 -0
- package/dist/server/config/index.js.map +1 -0
- package/dist/server/config/load.d.ts +21 -0
- package/dist/server/config/load.js +106 -0
- package/dist/server/config/load.js.map +1 -0
- package/dist/server/config/paths.d.ts +9 -0
- package/dist/server/config/paths.js +26 -0
- package/dist/server/config/paths.js.map +1 -0
- package/dist/server/config/schema.d.ts +26 -0
- package/dist/server/config/schema.js +56 -0
- package/dist/server/config/schema.js.map +1 -0
- package/dist/server/config/write.d.ts +29 -0
- package/dist/server/config/write.js +74 -0
- package/dist/server/config/write.js.map +1 -0
- package/dist/server/connection-file.d.ts +22 -0
- package/dist/server/connection-file.js +72 -0
- package/dist/server/connection-file.js.map +1 -0
- package/dist/server/dirty-buffer.d.ts +7 -0
- package/dist/server/dirty-buffer.js +39 -0
- package/dist/server/dirty-buffer.js.map +1 -0
- package/dist/server/files.d.ts +19 -0
- package/dist/server/files.js +112 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/hash.d.ts +1 -0
- package/dist/server/hash.js +7 -0
- package/dist/server/hash.js.map +1 -0
- package/dist/server/init.d.ts +16 -0
- package/dist/server/init.js +53 -0
- package/dist/server/init.js.map +1 -0
- package/dist/server/log.d.ts +8 -0
- package/dist/server/log.js +19 -0
- package/dist/server/log.js.map +1 -0
- package/dist/server/marker-ids.d.ts +3 -0
- package/dist/server/marker-ids.js +31 -0
- package/dist/server/marker-ids.js.map +1 -0
- package/dist/server/mcp-server.d.ts +22 -0
- package/dist/server/mcp-server.js +469 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/server/mention-ops.d.ts +115 -0
- package/dist/server/mention-ops.js +202 -0
- package/dist/server/mention-ops.js.map +1 -0
- package/dist/server/mention-store.d.ts +123 -0
- package/dist/server/mention-store.js +91 -0
- package/dist/server/mention-store.js.map +1 -0
- package/dist/server/runtime-check.d.ts +1 -0
- package/dist/server/runtime-check.js +10 -0
- package/dist/server/runtime-check.js.map +1 -0
- package/dist/server/server.d.ts +28 -0
- package/dist/server/server.js +736 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/stdio.d.ts +18 -0
- package/dist/server/stdio.js +144 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/server/verbs/builtin.d.ts +16 -0
- package/dist/server/verbs/builtin.js +27 -0
- package/dist/server/verbs/builtin.js.map +1 -0
- package/dist/server/verbs/index.d.ts +2 -0
- package/dist/server/verbs/index.js +3 -0
- package/dist/server/verbs/index.js.map +1 -0
- package/dist/server/verbs/load.d.ts +7 -0
- package/dist/server/verbs/load.js +24 -0
- package/dist/server/verbs/load.js.map +1 -0
- package/dist/server/workspace.d.ts +19 -0
- package/dist/server/workspace.js +138 -0
- package/dist/server/workspace.js.map +1 -0
- package/dist/shared/backoff.d.ts +1 -0
- package/dist/shared/backoff.js +12 -0
- package/dist/shared/backoff.js.map +1 -0
- package/dist/shared/markers.d.ts +136 -0
- package/dist/shared/markers.js +497 -0
- package/dist/shared/markers.js.map +1 -0
- package/dist/shared/protocol.d.ts +201 -0
- package/dist/shared/protocol.js +4 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/static/assets/apl-B4CMkyY2.js +2 -0
- package/dist/static/assets/apl-B4CMkyY2.js.map +1 -0
- package/dist/static/assets/asciiarmor-Df11BRmG.js +2 -0
- package/dist/static/assets/asciiarmor-Df11BRmG.js.map +1 -0
- package/dist/static/assets/asn1-EdZsLKOL.js +2 -0
- package/dist/static/assets/asn1-EdZsLKOL.js.map +1 -0
- package/dist/static/assets/asterisk-B-8jnY81.js +2 -0
- package/dist/static/assets/asterisk-B-8jnY81.js.map +1 -0
- package/dist/static/assets/brainfuck-C4LP7Hcl.js +2 -0
- package/dist/static/assets/brainfuck-C4LP7Hcl.js.map +1 -0
- package/dist/static/assets/clike-B9uivgTg.js +2 -0
- package/dist/static/assets/clike-B9uivgTg.js.map +1 -0
- package/dist/static/assets/clojure-BMjYHr_A.js +2 -0
- package/dist/static/assets/clojure-BMjYHr_A.js.map +1 -0
- package/dist/static/assets/cmake-BQqOBYOt.js +2 -0
- package/dist/static/assets/cmake-BQqOBYOt.js.map +1 -0
- package/dist/static/assets/cobol-CWcv1MsR.js +2 -0
- package/dist/static/assets/cobol-CWcv1MsR.js.map +1 -0
- package/dist/static/assets/coffeescript-S37ZYGWr.js +2 -0
- package/dist/static/assets/coffeescript-S37ZYGWr.js.map +1 -0
- package/dist/static/assets/commonlisp-DBKNyK5s.js +2 -0
- package/dist/static/assets/commonlisp-DBKNyK5s.js.map +1 -0
- package/dist/static/assets/crystal-SjHAIU92.js +2 -0
- package/dist/static/assets/crystal-SjHAIU92.js.map +1 -0
- package/dist/static/assets/css-BnMrqG3P.js +2 -0
- package/dist/static/assets/css-BnMrqG3P.js.map +1 -0
- package/dist/static/assets/cypher-C_CwsFkJ.js +2 -0
- package/dist/static/assets/cypher-C_CwsFkJ.js.map +1 -0
- package/dist/static/assets/d-pRatUO7H.js +2 -0
- package/dist/static/assets/d-pRatUO7H.js.map +1 -0
- package/dist/static/assets/diff-DbItnlRl.js +2 -0
- package/dist/static/assets/diff-DbItnlRl.js.map +1 -0
- package/dist/static/assets/dockerfile-BKs6k2Af.js +2 -0
- package/dist/static/assets/dockerfile-BKs6k2Af.js.map +1 -0
- package/dist/static/assets/dtd-DF_7sFjM.js +2 -0
- package/dist/static/assets/dtd-DF_7sFjM.js.map +1 -0
- package/dist/static/assets/dylan-DwRh75JA.js +2 -0
- package/dist/static/assets/dylan-DwRh75JA.js.map +1 -0
- package/dist/static/assets/ebnf-CDyGwa7X.js +2 -0
- package/dist/static/assets/ebnf-CDyGwa7X.js.map +1 -0
- package/dist/static/assets/ecl-Cabwm37j.js +2 -0
- package/dist/static/assets/ecl-Cabwm37j.js.map +1 -0
- package/dist/static/assets/eiffel-CnydiIhH.js +2 -0
- package/dist/static/assets/eiffel-CnydiIhH.js.map +1 -0
- package/dist/static/assets/elm-vLlmbW-K.js +2 -0
- package/dist/static/assets/elm-vLlmbW-K.js.map +1 -0
- package/dist/static/assets/erlang-BNw1qcRV.js +2 -0
- package/dist/static/assets/erlang-BNw1qcRV.js.map +1 -0
- package/dist/static/assets/factor-kuTfRLto.js +2 -0
- package/dist/static/assets/factor-kuTfRLto.js.map +1 -0
- package/dist/static/assets/fcl-Kvtd6kyn.js +2 -0
- package/dist/static/assets/fcl-Kvtd6kyn.js.map +1 -0
- package/dist/static/assets/forth-Ffai-XNe.js +2 -0
- package/dist/static/assets/forth-Ffai-XNe.js.map +1 -0
- package/dist/static/assets/fortran-DYz_wnZ1.js +2 -0
- package/dist/static/assets/fortran-DYz_wnZ1.js.map +1 -0
- package/dist/static/assets/gas-Bneqetm1.js +2 -0
- package/dist/static/assets/gas-Bneqetm1.js.map +1 -0
- package/dist/static/assets/gherkin-heZmZLOM.js +2 -0
- package/dist/static/assets/gherkin-heZmZLOM.js.map +1 -0
- package/dist/static/assets/groovy-D9Dt4D0W.js +2 -0
- package/dist/static/assets/groovy-D9Dt4D0W.js.map +1 -0
- package/dist/static/assets/haskell-BWDZoCOh.js +2 -0
- package/dist/static/assets/haskell-BWDZoCOh.js.map +1 -0
- package/dist/static/assets/haxe-H-WmDvRZ.js +2 -0
- package/dist/static/assets/haxe-H-WmDvRZ.js.map +1 -0
- package/dist/static/assets/http-DBlCnlav.js +2 -0
- package/dist/static/assets/http-DBlCnlav.js.map +1 -0
- package/dist/static/assets/idl-BEugSyMb.js +2 -0
- package/dist/static/assets/idl-BEugSyMb.js.map +1 -0
- package/dist/static/assets/index-76gOt96S.js +2 -0
- package/dist/static/assets/index-76gOt96S.js.map +1 -0
- package/dist/static/assets/index-BOQmpwuG.js +2 -0
- package/dist/static/assets/index-BOQmpwuG.js.map +1 -0
- package/dist/static/assets/index-BRr3mRH4.js +2 -0
- package/dist/static/assets/index-BRr3mRH4.js.map +1 -0
- package/dist/static/assets/index-B_hEIVV3.js +2 -0
- package/dist/static/assets/index-B_hEIVV3.js.map +1 -0
- package/dist/static/assets/index-Be89YI4R.js +2 -0
- package/dist/static/assets/index-Be89YI4R.js.map +1 -0
- package/dist/static/assets/index-BiZobR_v.js +4 -0
- package/dist/static/assets/index-BiZobR_v.js.map +1 -0
- package/dist/static/assets/index-Brumy5_9.js +8 -0
- package/dist/static/assets/index-Brumy5_9.js.map +1 -0
- package/dist/static/assets/index-BsJm14K9.js +2 -0
- package/dist/static/assets/index-BsJm14K9.js.map +1 -0
- package/dist/static/assets/index-CTGcuUyU.js +2 -0
- package/dist/static/assets/index-CTGcuUyU.js.map +1 -0
- package/dist/static/assets/index-Cvxvsst9.js +2 -0
- package/dist/static/assets/index-Cvxvsst9.js.map +1 -0
- package/dist/static/assets/index-D6ORLFAQ.js +2 -0
- package/dist/static/assets/index-D6ORLFAQ.js.map +1 -0
- package/dist/static/assets/index-DEhB0VAu.js +3 -0
- package/dist/static/assets/index-DEhB0VAu.js.map +1 -0
- package/dist/static/assets/index-DTNng05d.js +2 -0
- package/dist/static/assets/index-DTNng05d.js.map +1 -0
- package/dist/static/assets/index-DUOyyduC.js +2 -0
- package/dist/static/assets/index-DUOyyduC.js.map +1 -0
- package/dist/static/assets/index-DaO1DHpW.js +101 -0
- package/dist/static/assets/index-DaO1DHpW.js.map +1 -0
- package/dist/static/assets/index-DjRybwG9.js +2 -0
- package/dist/static/assets/index-DjRybwG9.js.map +1 -0
- package/dist/static/assets/index-MU4h_pJS.js +2 -0
- package/dist/static/assets/index-MU4h_pJS.js.map +1 -0
- package/dist/static/assets/index-PYTD2O0j.js +2 -0
- package/dist/static/assets/index-PYTD2O0j.js.map +1 -0
- package/dist/static/assets/index-SfxJH5Y4.css +1 -0
- package/dist/static/assets/javascript-qCveANmP.js +2 -0
- package/dist/static/assets/javascript-qCveANmP.js.map +1 -0
- package/dist/static/assets/julia-DuME0IfC.js +2 -0
- package/dist/static/assets/julia-DuME0IfC.js.map +1 -0
- package/dist/static/assets/livescript-BwQOo05w.js +2 -0
- package/dist/static/assets/livescript-BwQOo05w.js.map +1 -0
- package/dist/static/assets/lua-BgMRiT3U.js +2 -0
- package/dist/static/assets/lua-BgMRiT3U.js.map +1 -0
- package/dist/static/assets/mathematica-DTrFuWx2.js +2 -0
- package/dist/static/assets/mathematica-DTrFuWx2.js.map +1 -0
- package/dist/static/assets/mbox-CNhZ1qSd.js +2 -0
- package/dist/static/assets/mbox-CNhZ1qSd.js.map +1 -0
- package/dist/static/assets/mirc-CjQqDB4T.js +2 -0
- package/dist/static/assets/mirc-CjQqDB4T.js.map +1 -0
- package/dist/static/assets/mllike-CXdrOF99.js +2 -0
- package/dist/static/assets/mllike-CXdrOF99.js.map +1 -0
- package/dist/static/assets/modelica-Dc1JOy9r.js +2 -0
- package/dist/static/assets/modelica-Dc1JOy9r.js.map +1 -0
- package/dist/static/assets/mscgen-BA5vi2Kp.js +2 -0
- package/dist/static/assets/mscgen-BA5vi2Kp.js.map +1 -0
- package/dist/static/assets/mumps-BT43cFF4.js +2 -0
- package/dist/static/assets/mumps-BT43cFF4.js.map +1 -0
- package/dist/static/assets/nginx-DdIZxoE0.js +2 -0
- package/dist/static/assets/nginx-DdIZxoE0.js.map +1 -0
- package/dist/static/assets/nsis-LdVXkNf5.js +2 -0
- package/dist/static/assets/nsis-LdVXkNf5.js.map +1 -0
- package/dist/static/assets/ntriples-BfvgReVJ.js +2 -0
- package/dist/static/assets/ntriples-BfvgReVJ.js.map +1 -0
- package/dist/static/assets/octave-Ck1zUtKM.js +2 -0
- package/dist/static/assets/octave-Ck1zUtKM.js.map +1 -0
- package/dist/static/assets/oz-BzwKVEFT.js +2 -0
- package/dist/static/assets/oz-BzwKVEFT.js.map +1 -0
- package/dist/static/assets/pascal--L3eBynH.js +2 -0
- package/dist/static/assets/pascal--L3eBynH.js.map +1 -0
- package/dist/static/assets/perl-CdXCOZ3F.js +2 -0
- package/dist/static/assets/perl-CdXCOZ3F.js.map +1 -0
- package/dist/static/assets/pig-CevX1Tat.js +2 -0
- package/dist/static/assets/pig-CevX1Tat.js.map +1 -0
- package/dist/static/assets/powershell-CFHJl5sT.js +2 -0
- package/dist/static/assets/powershell-CFHJl5sT.js.map +1 -0
- package/dist/static/assets/properties-C78fOPTZ.js +2 -0
- package/dist/static/assets/properties-C78fOPTZ.js.map +1 -0
- package/dist/static/assets/protobuf-ChK-085T.js +2 -0
- package/dist/static/assets/protobuf-ChK-085T.js.map +1 -0
- package/dist/static/assets/pug-DukmZTjD.js +2 -0
- package/dist/static/assets/pug-DukmZTjD.js.map +1 -0
- package/dist/static/assets/puppet-DMA9R1ak.js +2 -0
- package/dist/static/assets/puppet-DMA9R1ak.js.map +1 -0
- package/dist/static/assets/python-BuPzkPfP.js +2 -0
- package/dist/static/assets/python-BuPzkPfP.js.map +1 -0
- package/dist/static/assets/q-pXgVlZs6.js +2 -0
- package/dist/static/assets/q-pXgVlZs6.js.map +1 -0
- package/dist/static/assets/r-DUYO_cvP.js +2 -0
- package/dist/static/assets/r-DUYO_cvP.js.map +1 -0
- package/dist/static/assets/rpm-CTu-6PCP.js +2 -0
- package/dist/static/assets/rpm-CTu-6PCP.js.map +1 -0
- package/dist/static/assets/ruby-B2Rjki9n.js +2 -0
- package/dist/static/assets/ruby-B2Rjki9n.js.map +1 -0
- package/dist/static/assets/sas-B4kiWyti.js +2 -0
- package/dist/static/assets/sas-B4kiWyti.js.map +1 -0
- package/dist/static/assets/scheme-C41bIUwD.js +2 -0
- package/dist/static/assets/scheme-C41bIUwD.js.map +1 -0
- package/dist/static/assets/shell-CjFT_Tl9.js +2 -0
- package/dist/static/assets/shell-CjFT_Tl9.js.map +1 -0
- package/dist/static/assets/sieve-C3Gn_uJK.js +2 -0
- package/dist/static/assets/sieve-C3Gn_uJK.js.map +1 -0
- package/dist/static/assets/simple-mode-GW_nhZxv.js +2 -0
- package/dist/static/assets/simple-mode-GW_nhZxv.js.map +1 -0
- package/dist/static/assets/smalltalk-CnHTOXQT.js +2 -0
- package/dist/static/assets/smalltalk-CnHTOXQT.js.map +1 -0
- package/dist/static/assets/solr-DehyRSwq.js +2 -0
- package/dist/static/assets/solr-DehyRSwq.js.map +1 -0
- package/dist/static/assets/sparql-DkYu6x3z.js +2 -0
- package/dist/static/assets/sparql-DkYu6x3z.js.map +1 -0
- package/dist/static/assets/spreadsheet-BCZA_wO0.js +2 -0
- package/dist/static/assets/spreadsheet-BCZA_wO0.js.map +1 -0
- package/dist/static/assets/sql-D0XecflT.js +2 -0
- package/dist/static/assets/sql-D0XecflT.js.map +1 -0
- package/dist/static/assets/stex-C3f8Ysf7.js +2 -0
- package/dist/static/assets/stex-C3f8Ysf7.js.map +1 -0
- package/dist/static/assets/stylus-B533Al4x.js +2 -0
- package/dist/static/assets/stylus-B533Al4x.js.map +1 -0
- package/dist/static/assets/swift-BzpIVaGY.js +2 -0
- package/dist/static/assets/swift-BzpIVaGY.js.map +1 -0
- package/dist/static/assets/tcl-DVfN8rqt.js +2 -0
- package/dist/static/assets/tcl-DVfN8rqt.js.map +1 -0
- package/dist/static/assets/textile-CnDTJFAw.js +2 -0
- package/dist/static/assets/textile-CnDTJFAw.js.map +1 -0
- package/dist/static/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- package/dist/static/assets/tiddlywiki-DO-Gjzrf.js.map +1 -0
- package/dist/static/assets/tiki-DGYXhP31.js +2 -0
- package/dist/static/assets/tiki-DGYXhP31.js.map +1 -0
- package/dist/static/assets/toml-Bm5Em-hy.js +2 -0
- package/dist/static/assets/toml-Bm5Em-hy.js.map +1 -0
- package/dist/static/assets/troff-wAsdV37c.js +2 -0
- package/dist/static/assets/troff-wAsdV37c.js.map +1 -0
- package/dist/static/assets/ttcn-CfJYG6tj.js +2 -0
- package/dist/static/assets/ttcn-CfJYG6tj.js.map +1 -0
- package/dist/static/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- package/dist/static/assets/ttcn-cfg-B9xdYoR4.js.map +1 -0
- package/dist/static/assets/turtle-B1tBg_DP.js +2 -0
- package/dist/static/assets/turtle-B1tBg_DP.js.map +1 -0
- package/dist/static/assets/vb-CmGdzxic.js +2 -0
- package/dist/static/assets/vb-CmGdzxic.js.map +1 -0
- package/dist/static/assets/vbscript-BuJXcnF6.js +2 -0
- package/dist/static/assets/vbscript-BuJXcnF6.js.map +1 -0
- package/dist/static/assets/velocity-D8B20fx6.js +2 -0
- package/dist/static/assets/velocity-D8B20fx6.js.map +1 -0
- package/dist/static/assets/verilog-C6RDOZhf.js +2 -0
- package/dist/static/assets/verilog-C6RDOZhf.js.map +1 -0
- package/dist/static/assets/vhdl-lSbBsy5d.js +2 -0
- package/dist/static/assets/vhdl-lSbBsy5d.js.map +1 -0
- package/dist/static/assets/webidl-ZXfAyPTL.js +2 -0
- package/dist/static/assets/webidl-ZXfAyPTL.js.map +1 -0
- package/dist/static/assets/xquery-CQfU5ijd.js +2 -0
- package/dist/static/assets/xquery-CQfU5ijd.js.map +1 -0
- package/dist/static/assets/yacas-BJ4BC0dw.js +2 -0
- package/dist/static/assets/yacas-BJ4BC0dw.js.map +1 -0
- package/dist/static/assets/z80-Hz9HOZM7.js +2 -0
- package/dist/static/assets/z80-Hz9HOZM7.js.map +1 -0
- package/dist/static/index.html +14 -0
- package/package.json +89 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { extname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
import { acceptSuggestion, addAnnotation, removeAnnotation, } from "./annotation-ops.js";
|
|
7
|
+
import { resolveHumanAuthor } from "./author.js";
|
|
8
|
+
import { createDirtyBufferTracker } from "./dirty-buffer.js";
|
|
9
|
+
import { createFile, createFolder, deletePath, readWorkspaceFile, renamePath, saveWorkspaceFile, } from "./files.js";
|
|
10
|
+
import { log } from "./log.js";
|
|
11
|
+
import { createSidebarMcpServer } from "./mcp-server.js";
|
|
12
|
+
import { cancelMention, createMention, listMentions, warnOnceForMalformed, } from "./mention-ops.js";
|
|
13
|
+
import { createMentionStore } from "./mention-store.js";
|
|
14
|
+
import { buildTree, startWatcher } from "./workspace.js";
|
|
15
|
+
export async function startServer(opts) {
|
|
16
|
+
const { workspace, verbCatalog } = opts;
|
|
17
|
+
const host = opts.host ?? "127.0.0.1";
|
|
18
|
+
const staticRoot = opts.staticRoot ?? null;
|
|
19
|
+
const dirtyBuffers = createDirtyBufferTracker();
|
|
20
|
+
const mentionStore = createMentionStore();
|
|
21
|
+
const mentionCreatedAt = new Map();
|
|
22
|
+
const annotationCreatedAt = new Map();
|
|
23
|
+
const warnedMalformedFiles = new Set();
|
|
24
|
+
const mcpDeps = {
|
|
25
|
+
workspace,
|
|
26
|
+
dirtyBuffers,
|
|
27
|
+
mentionStore,
|
|
28
|
+
mentionCreatedAt,
|
|
29
|
+
annotationCreatedAt,
|
|
30
|
+
verbCatalog,
|
|
31
|
+
resolveHumanAuthor: () => resolveHumanAuthor(workspace.root),
|
|
32
|
+
};
|
|
33
|
+
const http = createServer((req, res) => {
|
|
34
|
+
void route(req, res).catch((e) => {
|
|
35
|
+
try {
|
|
36
|
+
res.statusCode = 500;
|
|
37
|
+
res.end(`internal error: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* socket already gone */
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
// Streamable HTTP MCP transport mounts onto the existing node:http server
|
|
45
|
+
// (ADR-0008). Stateless mode: one fresh transport + McpServer per POST.
|
|
46
|
+
// Spec: Architecture / Invocation modes — standalone exposes the MCP
|
|
47
|
+
// server over HTTP at /mcp so additional agents can attach.
|
|
48
|
+
const handleMcpHttp = async (req, res) => {
|
|
49
|
+
if (req.method !== "POST") {
|
|
50
|
+
res.statusCode = 405;
|
|
51
|
+
res.setHeader("content-type", "application/json");
|
|
52
|
+
res.end(JSON.stringify({
|
|
53
|
+
jsonrpc: "2.0",
|
|
54
|
+
error: { code: -32000, message: "Only POST is supported on /mcp" },
|
|
55
|
+
id: null,
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
60
|
+
const mcp = createSidebarMcpServer(mcpDeps);
|
|
61
|
+
try {
|
|
62
|
+
await mcp.connect(transport);
|
|
63
|
+
await transport.handleRequest(req, res);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
log.warn(`mcp http handler error: ${e.message}`);
|
|
67
|
+
if (!res.headersSent) {
|
|
68
|
+
res.statusCode = 500;
|
|
69
|
+
res.end(JSON.stringify({
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
error: { code: -32603, message: "internal error" },
|
|
72
|
+
id: null,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
// The stateless transport is single-request; close after we are done.
|
|
78
|
+
void transport.close().catch(() => { });
|
|
79
|
+
void mcp.close().catch(() => { });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const route = async (req, res) => {
|
|
83
|
+
const url = req.url ?? "/";
|
|
84
|
+
if (url === "/mcp" || url.startsWith("/mcp?")) {
|
|
85
|
+
await handleMcpHttp(req, res);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await handleHttp(req, res, staticRoot);
|
|
89
|
+
};
|
|
90
|
+
// Bind the port first. The HTTP listener surfaces EADDRINUSE here, before
|
|
91
|
+
// any WebSocketServer or watcher gets created — bailing early keeps a port
|
|
92
|
+
// collision from leaking partial resources.
|
|
93
|
+
const port = await listen(http, opts.port, host);
|
|
94
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
95
|
+
const allowedOrigins = originAllowlistFor(host, port);
|
|
96
|
+
http.on("upgrade", (req, socket, head) => {
|
|
97
|
+
// CSRF defense: the WebSocket protocol can read and mutate workspace
|
|
98
|
+
// files. A malicious public page can fetch `ws://127.0.0.1:<port>/ws`
|
|
99
|
+
// from any browser the user opens. Only allow upgrades that either
|
|
100
|
+
// (a) carry no Origin header (non-browser clients, including our tests
|
|
101
|
+
// and curl), or (b) carry an Origin that matches the sidebar listener.
|
|
102
|
+
const origin = req.headers.origin;
|
|
103
|
+
if (origin !== undefined && !allowedOrigins.has(origin)) {
|
|
104
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
105
|
+
socket.destroy();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (req.url === "/ws" || req.url?.startsWith("/ws?")) {
|
|
109
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
socket.destroy();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const clients = new Set();
|
|
116
|
+
const broadcast = (msg) => {
|
|
117
|
+
const json = JSON.stringify(msg);
|
|
118
|
+
for (const c of clients) {
|
|
119
|
+
if (c.readyState === c.OPEN)
|
|
120
|
+
c.send(json);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const refreshTree = async () => {
|
|
124
|
+
try {
|
|
125
|
+
const nodes = await buildTree(workspace);
|
|
126
|
+
broadcast({ kind: "treeChanged", nodes });
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
log.warn(`buildTree failed: ${e.message}`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const notifyExternalChange = async (relPath) => {
|
|
133
|
+
try {
|
|
134
|
+
const { content, hash } = await readWorkspaceFile(workspace, relPath);
|
|
135
|
+
broadcast({ kind: "diskChanged", path: relPath, content, diskHash: hash });
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
const code = e.code;
|
|
139
|
+
if (code === "ENOENT") {
|
|
140
|
+
broadcast({ kind: "diskRemoved", path: relPath });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
log.warn(`readWorkspaceFile failed for ${relPath}: ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A change to a file may have added/removed mention markers; recompute
|
|
147
|
+
// the status snapshot so the drawer reflects the new world.
|
|
148
|
+
void broadcastStatus();
|
|
149
|
+
};
|
|
150
|
+
const buildStatusSnapshot = async () => {
|
|
151
|
+
const { mentions, malformedByFile } = await listMentions(workspace, mentionCreatedAt);
|
|
152
|
+
if (malformedByFile.size > 0) {
|
|
153
|
+
// One stderr warning per file, only on first observation.
|
|
154
|
+
const fresh = new Map();
|
|
155
|
+
for (const [f, errs] of malformedByFile) {
|
|
156
|
+
if (!warnedMalformedFiles.has(f)) {
|
|
157
|
+
warnedMalformedFiles.add(f);
|
|
158
|
+
fresh.set(f, errs);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (fresh.size > 0) {
|
|
162
|
+
// biome-ignore lint/suspicious/noExplicitAny: shape matches the imported type
|
|
163
|
+
warnOnceForMalformed(fresh);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const claims = new Map(mentionStore.claims().map((c) => [c.mentionId, c]));
|
|
167
|
+
return {
|
|
168
|
+
pendingMentions: mentions.map((m) => {
|
|
169
|
+
const claim = claims.get(m.id);
|
|
170
|
+
return {
|
|
171
|
+
id: m.id,
|
|
172
|
+
file: m.file,
|
|
173
|
+
origin: m.origin,
|
|
174
|
+
verb: m.verb,
|
|
175
|
+
author: m.author,
|
|
176
|
+
instruction: m.instruction,
|
|
177
|
+
orphan: m.orphan,
|
|
178
|
+
created_at: m.created_at,
|
|
179
|
+
inProgress: claim ? { agent: claim.agentName, claimedAt: claim.claimedAt } : undefined,
|
|
180
|
+
};
|
|
181
|
+
}),
|
|
182
|
+
connectedAgents: mentionStore.connectedAgents().map((a) => ({
|
|
183
|
+
name: a.name,
|
|
184
|
+
connectedAt: a.connectedAt,
|
|
185
|
+
})),
|
|
186
|
+
recentEvents: mentionStore.recentEvents().map((e) => ({ ...e })),
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
const broadcastStatus = async () => {
|
|
190
|
+
try {
|
|
191
|
+
const snapshot = await buildStatusSnapshot();
|
|
192
|
+
broadcast({ kind: "status", snapshot });
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
log.warn(`status broadcast failed: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
// Anything that changes the mention store (claim, release, event) should
|
|
199
|
+
// push a fresh status snapshot to subscribed editor tabs.
|
|
200
|
+
mentionStore.onChange(() => {
|
|
201
|
+
void broadcastStatus();
|
|
202
|
+
});
|
|
203
|
+
const watcher = startWatcher(workspace, () => void refreshTree(), (relPath) => void notifyExternalChange(relPath));
|
|
204
|
+
// Wait for chokidar's initial scan so tests (and the first browser load)
|
|
205
|
+
// never race a not-yet-ready watcher.
|
|
206
|
+
await new Promise((res) => watcher.once("ready", () => res()));
|
|
207
|
+
wss.on("connection", (ws) => {
|
|
208
|
+
clients.add(ws);
|
|
209
|
+
// Track paths this tab declared dirty. When the tab disconnects we
|
|
210
|
+
// unmark them so a closed editor never leaves a phantom draft visible
|
|
211
|
+
// to MCP clients via `read_doc`.
|
|
212
|
+
const myDirty = new Set();
|
|
213
|
+
ws.on("close", () => {
|
|
214
|
+
for (const p of myDirty)
|
|
215
|
+
dirtyBuffers.setDirty(p, false);
|
|
216
|
+
clients.delete(ws);
|
|
217
|
+
});
|
|
218
|
+
ws.on("message", (raw) => {
|
|
219
|
+
let msg;
|
|
220
|
+
try {
|
|
221
|
+
msg = JSON.parse(raw.toString());
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
ws.send(JSON.stringify({ kind: "error", message: "invalid JSON" }));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Catch-all: every per-case handler already converts errors to a
|
|
228
|
+
// `kind: "error"` reply, but anything that throws synchronously or
|
|
229
|
+
// before the inner try/catch (e.g. `list` building the tree) would
|
|
230
|
+
// otherwise become an unhandled rejection. Belt and suspenders.
|
|
231
|
+
if (msg.kind === "dirty") {
|
|
232
|
+
if (msg.isDirty)
|
|
233
|
+
myDirty.add(msg.path);
|
|
234
|
+
else
|
|
235
|
+
myDirty.delete(msg.path);
|
|
236
|
+
}
|
|
237
|
+
void handleMessage(msg, ws, {
|
|
238
|
+
workspace,
|
|
239
|
+
refreshTree,
|
|
240
|
+
dirtyBuffers,
|
|
241
|
+
mentionStore,
|
|
242
|
+
mentionCreatedAt,
|
|
243
|
+
annotationCreatedAt,
|
|
244
|
+
verbCatalog,
|
|
245
|
+
broadcastStatus,
|
|
246
|
+
}).catch((e) => {
|
|
247
|
+
try {
|
|
248
|
+
ws.send(JSON.stringify({
|
|
249
|
+
kind: "error",
|
|
250
|
+
message: `unhandled error processing ${msg.kind}`,
|
|
251
|
+
cause: e.message,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
/* socket may already be closed */
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
ws.send(JSON.stringify({
|
|
260
|
+
kind: "welcome",
|
|
261
|
+
workspaceRoot: workspace.root,
|
|
262
|
+
scope: workspace.scope,
|
|
263
|
+
}));
|
|
264
|
+
void buildTree(workspace).then((nodes) => ws.send(JSON.stringify({ kind: "tree", nodes })), (e) => ws.send(JSON.stringify({
|
|
265
|
+
kind: "error",
|
|
266
|
+
message: "tree build failed",
|
|
267
|
+
cause: e.message,
|
|
268
|
+
})));
|
|
269
|
+
});
|
|
270
|
+
const url = `http://${host}:${port}`;
|
|
271
|
+
return {
|
|
272
|
+
url,
|
|
273
|
+
port,
|
|
274
|
+
workspace,
|
|
275
|
+
dirtyBuffers,
|
|
276
|
+
mentionStore,
|
|
277
|
+
mentionCreatedAt,
|
|
278
|
+
annotationCreatedAt,
|
|
279
|
+
verbCatalog,
|
|
280
|
+
close: async () => {
|
|
281
|
+
// Terminate live WebSocket clients before closing the HTTP server, or
|
|
282
|
+
// http.close() will wait for the existing upgraded sockets to drain on
|
|
283
|
+
// their own — which, with an editor tab still open, never happens.
|
|
284
|
+
for (const c of clients) {
|
|
285
|
+
try {
|
|
286
|
+
c.terminate();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* socket already gone */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
await watcher.close();
|
|
293
|
+
await new Promise((res) => wss.close(() => res()));
|
|
294
|
+
http.closeAllConnections();
|
|
295
|
+
await new Promise((res) => http.close(() => res()));
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function listen(server, port, host) {
|
|
300
|
+
return new Promise((res, rej) => {
|
|
301
|
+
const onError = (e) => {
|
|
302
|
+
server.off("listening", onListening);
|
|
303
|
+
rej(e);
|
|
304
|
+
};
|
|
305
|
+
const onListening = () => {
|
|
306
|
+
server.off("error", onError);
|
|
307
|
+
const addr = server.address();
|
|
308
|
+
if (addr && typeof addr === "object")
|
|
309
|
+
res(addr.port);
|
|
310
|
+
else
|
|
311
|
+
rej(new Error("server listen returned no address"));
|
|
312
|
+
};
|
|
313
|
+
server.once("error", onError);
|
|
314
|
+
server.once("listening", onListening);
|
|
315
|
+
server.listen(port, host);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async function handleMessage(msg, ws, deps) {
|
|
319
|
+
const { workspace, refreshTree, dirtyBuffers, mentionStore, mentionCreatedAt, annotationCreatedAt, verbCatalog, broadcastStatus, } = deps;
|
|
320
|
+
const send = (m) => ws.send(JSON.stringify(m));
|
|
321
|
+
switch (msg.kind) {
|
|
322
|
+
case "hello":
|
|
323
|
+
return;
|
|
324
|
+
case "dirty":
|
|
325
|
+
dirtyBuffers.setDirty(msg.path, msg.isDirty);
|
|
326
|
+
return;
|
|
327
|
+
case "list": {
|
|
328
|
+
try {
|
|
329
|
+
const nodes = await buildTree(workspace);
|
|
330
|
+
send({ kind: "tree", nodes });
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
send({ kind: "error", message: "list failed", cause: e.message });
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
case "open":
|
|
338
|
+
try {
|
|
339
|
+
const { content, hash } = await readWorkspaceFile(workspace, msg.path);
|
|
340
|
+
send({ kind: "fileOpen", path: msg.path, content, diskHash: hash });
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
send({ kind: "error", message: `open failed: ${msg.path}`, cause: e.message });
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
case "save":
|
|
347
|
+
try {
|
|
348
|
+
const outcome = await saveWorkspaceFile(workspace, msg.path, msg.content, msg.baseHash);
|
|
349
|
+
if (outcome.kind === "saved") {
|
|
350
|
+
send({ kind: "saved", path: msg.path, diskHash: outcome.hash });
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
send({
|
|
354
|
+
kind: "saveConflict",
|
|
355
|
+
path: msg.path,
|
|
356
|
+
content: outcome.content,
|
|
357
|
+
diskHash: outcome.diskHash,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
send({ kind: "error", message: `save failed: ${msg.path}`, cause: e.message });
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
case "newFile":
|
|
366
|
+
try {
|
|
367
|
+
await createFile(workspace, msg.parent, msg.name);
|
|
368
|
+
await refreshTree();
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
send({ kind: "error", message: "newFile failed", cause: e.message });
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
case "newFolder":
|
|
375
|
+
try {
|
|
376
|
+
await createFolder(workspace, msg.parent, msg.name);
|
|
377
|
+
await refreshTree();
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
send({ kind: "error", message: "newFolder failed", cause: e.message });
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
case "rename":
|
|
384
|
+
try {
|
|
385
|
+
await renamePath(workspace, msg.from, msg.to);
|
|
386
|
+
await refreshTree();
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
send({ kind: "error", message: "rename failed", cause: e.message });
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
case "delete":
|
|
393
|
+
try {
|
|
394
|
+
await deletePath(workspace, msg.path);
|
|
395
|
+
await refreshTree();
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
send({ kind: "error", message: "delete failed", cause: e.message });
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
case "verbCatalog": {
|
|
402
|
+
const snapshot = {
|
|
403
|
+
human: Array.from(verbCatalog.human.values()).map((v) => ({
|
|
404
|
+
name: v.name,
|
|
405
|
+
kind: v.mode === "replace" ? "human-replace" : "human-annotation",
|
|
406
|
+
builtin: v.builtin,
|
|
407
|
+
})),
|
|
408
|
+
agent: Array.from(verbCatalog.agent.values()).map((v) => ({
|
|
409
|
+
name: v.name,
|
|
410
|
+
kind: "agent",
|
|
411
|
+
builtin: v.builtin,
|
|
412
|
+
})),
|
|
413
|
+
};
|
|
414
|
+
send({ kind: "verbCatalog", catalog: snapshot });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
case "statusRequest":
|
|
418
|
+
// Push the latest snapshot just to this caller.
|
|
419
|
+
await broadcastStatus();
|
|
420
|
+
return;
|
|
421
|
+
case "createMention":
|
|
422
|
+
try {
|
|
423
|
+
const author = resolveHumanAuthor(workspace.root);
|
|
424
|
+
const result = await createMention(workspace, {
|
|
425
|
+
path: msg.path,
|
|
426
|
+
startOffset: msg.startOffset,
|
|
427
|
+
endOffset: msg.endOffset,
|
|
428
|
+
verb: msg.verb,
|
|
429
|
+
instruction: msg.instruction,
|
|
430
|
+
author,
|
|
431
|
+
});
|
|
432
|
+
// Eagerly track creation timestamp so the next status snapshot
|
|
433
|
+
// already carries the fresh mention.
|
|
434
|
+
mentionCreatedAt.set(result.mention.id, new Date().toISOString());
|
|
435
|
+
mentionStore.recordEvent({
|
|
436
|
+
kind: "mention-created",
|
|
437
|
+
mention_id: result.mention.id,
|
|
438
|
+
file: msg.path,
|
|
439
|
+
verb: msg.verb,
|
|
440
|
+
origin: "human",
|
|
441
|
+
author,
|
|
442
|
+
at: new Date().toISOString(),
|
|
443
|
+
});
|
|
444
|
+
send({ kind: "mentionCreated", mentionId: result.mention.id, file: msg.path });
|
|
445
|
+
await broadcastStatus();
|
|
446
|
+
}
|
|
447
|
+
catch (e) {
|
|
448
|
+
send({
|
|
449
|
+
kind: "error",
|
|
450
|
+
message: "createMention failed",
|
|
451
|
+
cause: e.message,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
case "cancelMention":
|
|
456
|
+
try {
|
|
457
|
+
const outcome = await cancelMention(workspace, msg.mentionId, mentionCreatedAt);
|
|
458
|
+
if (outcome.kind === "not-found") {
|
|
459
|
+
send({
|
|
460
|
+
kind: "error",
|
|
461
|
+
message: "cancelMention failed",
|
|
462
|
+
cause: `no open mention with id ${msg.mentionId}`,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
mentionStore.release(msg.mentionId);
|
|
467
|
+
mentionCreatedAt.delete(msg.mentionId);
|
|
468
|
+
mentionStore.recordEvent({
|
|
469
|
+
kind: "mention-cancelled",
|
|
470
|
+
mention_id: msg.mentionId,
|
|
471
|
+
file: outcome.file,
|
|
472
|
+
at: new Date().toISOString(),
|
|
473
|
+
});
|
|
474
|
+
await broadcastStatus();
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
send({
|
|
478
|
+
kind: "error",
|
|
479
|
+
message: "cancelMention failed",
|
|
480
|
+
cause: e.message,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
case "releaseClaim": {
|
|
485
|
+
const released = mentionStore.release(msg.mentionId);
|
|
486
|
+
if (!released) {
|
|
487
|
+
send({
|
|
488
|
+
kind: "error",
|
|
489
|
+
message: "releaseClaim failed",
|
|
490
|
+
cause: `mention ${msg.mentionId} is not claimed`,
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
mentionStore.recordEvent({
|
|
495
|
+
kind: "mention-released",
|
|
496
|
+
mention_id: msg.mentionId,
|
|
497
|
+
file: "",
|
|
498
|
+
agent: released.agentName,
|
|
499
|
+
reason: "manual release from status drawer",
|
|
500
|
+
at: new Date().toISOString(),
|
|
501
|
+
});
|
|
502
|
+
await broadcastStatus();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
case "createAnnotation":
|
|
506
|
+
try {
|
|
507
|
+
const author = resolveHumanAuthor(workspace.root);
|
|
508
|
+
const result = await addAnnotation(workspace, {
|
|
509
|
+
path: msg.path,
|
|
510
|
+
target_anchor: { start: msg.startOffset, end: msg.endOffset },
|
|
511
|
+
type: msg.type,
|
|
512
|
+
content: msg.content,
|
|
513
|
+
author,
|
|
514
|
+
});
|
|
515
|
+
annotationCreatedAt.set(result.annotation.id, new Date().toISOString());
|
|
516
|
+
mentionStore.recordEvent({
|
|
517
|
+
kind: "annotation-created",
|
|
518
|
+
annotation_id: result.annotation.id,
|
|
519
|
+
file: msg.path,
|
|
520
|
+
annotation_type: msg.type,
|
|
521
|
+
author,
|
|
522
|
+
at: new Date().toISOString(),
|
|
523
|
+
});
|
|
524
|
+
send({
|
|
525
|
+
kind: "annotationCreated",
|
|
526
|
+
annotationId: result.annotation.id,
|
|
527
|
+
file: msg.path,
|
|
528
|
+
type: msg.type,
|
|
529
|
+
});
|
|
530
|
+
await broadcastStatus();
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
send({
|
|
534
|
+
kind: "error",
|
|
535
|
+
message: "createAnnotation failed",
|
|
536
|
+
cause: e.message,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
case "acceptSuggestion": {
|
|
541
|
+
const result = await acceptSuggestion(workspace, msg.annotationId, annotationCreatedAt);
|
|
542
|
+
if (result.kind === "not-found") {
|
|
543
|
+
send({
|
|
544
|
+
kind: "error",
|
|
545
|
+
message: "acceptSuggestion failed",
|
|
546
|
+
cause: `no annotation with id ${msg.annotationId}`,
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (result.kind === "not-suggestion") {
|
|
551
|
+
send({
|
|
552
|
+
kind: "error",
|
|
553
|
+
message: "acceptSuggestion failed",
|
|
554
|
+
cause: `annotation ${msg.annotationId} is not a suggestion`,
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
mentionStore.recordEvent({
|
|
559
|
+
kind: "suggestion-accepted",
|
|
560
|
+
annotation_id: msg.annotationId,
|
|
561
|
+
file: result.file,
|
|
562
|
+
author: result.author,
|
|
563
|
+
at: new Date().toISOString(),
|
|
564
|
+
});
|
|
565
|
+
await broadcastStatus();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
case "rejectSuggestion": {
|
|
569
|
+
const author = resolveHumanAuthor(workspace.root);
|
|
570
|
+
// Reject = remove the annotation pair. The human is the actor here so
|
|
571
|
+
// we do not enforce author scope on the underlying removal.
|
|
572
|
+
const result = await removeAnnotation(workspace, msg.annotationId, {
|
|
573
|
+
firstSeenAt: annotationCreatedAt,
|
|
574
|
+
});
|
|
575
|
+
void author;
|
|
576
|
+
if (result.kind === "not-found") {
|
|
577
|
+
send({
|
|
578
|
+
kind: "error",
|
|
579
|
+
message: "rejectSuggestion failed",
|
|
580
|
+
cause: `no annotation with id ${msg.annotationId}`,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (result.kind === "forbidden") {
|
|
585
|
+
// unreachable in this path (no requireAuthor), but keep the branch
|
|
586
|
+
// exhaustive so the discriminated union stays sound.
|
|
587
|
+
send({
|
|
588
|
+
kind: "error",
|
|
589
|
+
message: "rejectSuggestion failed",
|
|
590
|
+
cause: `annotation ${msg.annotationId} is owned by ${result.author}`,
|
|
591
|
+
});
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (result.type !== "suggestion") {
|
|
595
|
+
// Removing a note via the reject path is a contract violation; the
|
|
596
|
+
// UI only renders Accept/Reject on suggestion cards.
|
|
597
|
+
send({
|
|
598
|
+
kind: "error",
|
|
599
|
+
message: "rejectSuggestion failed",
|
|
600
|
+
cause: `annotation ${msg.annotationId} is a note, not a suggestion`,
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
mentionStore.recordEvent({
|
|
605
|
+
kind: "suggestion-rejected",
|
|
606
|
+
annotation_id: msg.annotationId,
|
|
607
|
+
file: result.file,
|
|
608
|
+
author: result.author,
|
|
609
|
+
at: new Date().toISOString(),
|
|
610
|
+
});
|
|
611
|
+
await broadcastStatus();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
case "removeAnnotation": {
|
|
615
|
+
const result = await removeAnnotation(workspace, msg.annotationId, {
|
|
616
|
+
firstSeenAt: annotationCreatedAt,
|
|
617
|
+
});
|
|
618
|
+
if (result.kind === "not-found") {
|
|
619
|
+
send({
|
|
620
|
+
kind: "error",
|
|
621
|
+
message: "removeAnnotation failed",
|
|
622
|
+
cause: `no annotation with id ${msg.annotationId}`,
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (result.kind === "forbidden") {
|
|
627
|
+
send({
|
|
628
|
+
kind: "error",
|
|
629
|
+
message: "removeAnnotation failed",
|
|
630
|
+
cause: `annotation ${msg.annotationId} is owned by ${result.author}`,
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
mentionStore.recordEvent({
|
|
635
|
+
kind: "annotation-removed",
|
|
636
|
+
annotation_id: msg.annotationId,
|
|
637
|
+
file: result.file,
|
|
638
|
+
annotation_type: result.type,
|
|
639
|
+
author: resolveHumanAuthor(workspace.root),
|
|
640
|
+
at: new Date().toISOString(),
|
|
641
|
+
});
|
|
642
|
+
await broadcastStatus();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function handleHttp(req, res, staticRoot) {
|
|
648
|
+
const url = req.url ?? "/";
|
|
649
|
+
if (url === "/healthz") {
|
|
650
|
+
res.statusCode = 200;
|
|
651
|
+
res.setHeader("content-type", "text/plain");
|
|
652
|
+
res.end("ok");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (!staticRoot) {
|
|
656
|
+
res.statusCode = 503;
|
|
657
|
+
res.setHeader("content-type", "text/plain");
|
|
658
|
+
res.end("sidebar editor SPA bundle is not present.\nRun `npm run build` first, then start sidebar again.\n");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const path = url.split("?")[0].replace(/^\/+/, "");
|
|
662
|
+
const candidate = path === "" ? "index.html" : path;
|
|
663
|
+
const root = resolve(staticRoot);
|
|
664
|
+
const abs = resolve(root, candidate);
|
|
665
|
+
// Sibling-path defense: `abs.startsWith(root)` accepts e.g. /foo/static-evil
|
|
666
|
+
// when root is /foo/static. Use path.relative so any escape resolves to a
|
|
667
|
+
// segment beginning with "..".
|
|
668
|
+
const rel = relative(root, abs);
|
|
669
|
+
if (rel.startsWith("..") || rel.startsWith(sep) || rel === "") {
|
|
670
|
+
// rel === "" is the root itself; we redirect that to index.html below,
|
|
671
|
+
// but reject anything else above the root.
|
|
672
|
+
if (rel !== "") {
|
|
673
|
+
res.statusCode = 403;
|
|
674
|
+
res.end("forbidden");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const s = await stat(abs);
|
|
680
|
+
if (s.isFile()) {
|
|
681
|
+
const body = await readFile(abs);
|
|
682
|
+
res.setHeader("content-type", contentTypeFor(abs));
|
|
683
|
+
res.end(body);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// fall through to index.html SPA shell
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const body = await readFile(resolve(staticRoot, "index.html"));
|
|
692
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
693
|
+
res.end(body);
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
res.statusCode = 404;
|
|
697
|
+
res.end("not found");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function originAllowlistFor(host, port) {
|
|
701
|
+
// The HTTP listener binds to either `127.0.0.1` (our default) or
|
|
702
|
+
// `localhost`-style aliases. Browsers normalize Origin from whatever the
|
|
703
|
+
// user navigated to. Accept the canonical IPv4 form, the IPv6 loopback,
|
|
704
|
+
// and `localhost`, all on the bound port.
|
|
705
|
+
const out = new Set([
|
|
706
|
+
`http://${host}:${port}`,
|
|
707
|
+
`http://127.0.0.1:${port}`,
|
|
708
|
+
`http://[::1]:${port}`,
|
|
709
|
+
`http://localhost:${port}`,
|
|
710
|
+
]);
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
function contentTypeFor(path) {
|
|
714
|
+
switch (extname(path)) {
|
|
715
|
+
case ".html":
|
|
716
|
+
return "text/html; charset=utf-8";
|
|
717
|
+
case ".js":
|
|
718
|
+
case ".mjs":
|
|
719
|
+
return "application/javascript; charset=utf-8";
|
|
720
|
+
case ".css":
|
|
721
|
+
return "text/css; charset=utf-8";
|
|
722
|
+
case ".json":
|
|
723
|
+
return "application/json; charset=utf-8";
|
|
724
|
+
case ".svg":
|
|
725
|
+
return "image/svg+xml";
|
|
726
|
+
case ".png":
|
|
727
|
+
return "image/png";
|
|
728
|
+
case ".woff2":
|
|
729
|
+
return "font/woff2";
|
|
730
|
+
case ".woff":
|
|
731
|
+
return "font/woff";
|
|
732
|
+
default:
|
|
733
|
+
return "application/octet-stream";
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
//# sourceMappingURL=server.js.map
|