@akira-tl/forgerelay 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/CHANGELOG.md +45 -0
- package/LICENSE +22 -0
- package/NOTICE.md +19 -0
- package/README.md +270 -0
- package/dist/apply-patch.js +402 -0
- package/dist/artifact-error.js +8 -0
- package/dist/artifact-tools.js +377 -0
- package/dist/cli.js +597 -0
- package/dist/config.js +224 -0
- package/dist/db/client.js +29 -0
- package/dist/db/migrations.js +189 -0
- package/dist/db/schema.js +85 -0
- package/dist/git-worktrees.js +255 -0
- package/dist/git.js +41 -0
- package/dist/incoming-artifacts.js +378 -0
- package/dist/local-agent-adapters.js +653 -0
- package/dist/local-agent-availability.js +119 -0
- package/dist/local-agent-path.js +25 -0
- package/dist/local-agent-profiles.js +136 -0
- package/dist/local-agent-runtime.js +48 -0
- package/dist/local-agent-store.js +145 -0
- package/dist/local-agent-targets.js +80 -0
- package/dist/logger.js +69 -0
- package/dist/mcp/server-instructions.js +78 -0
- package/dist/mcp-sessions.js +56 -0
- package/dist/oauth-provider.js +237 -0
- package/dist/oauth-store.js +138 -0
- package/dist/pi-tools.js +81 -0
- package/dist/process-platform.js +49 -0
- package/dist/process-sessions.js +337 -0
- package/dist/request-meta.js +9 -0
- package/dist/review-checkpoints.js +219 -0
- package/dist/roots.js +37 -0
- package/dist/server-shutdown.js +12 -0
- package/dist/server.js +1596 -0
- package/dist/skills.js +89 -0
- package/dist/ui/.vite/manifest.json +2521 -0
- package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
- package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
- package/dist/ui/assets/ada-C5qYipkI.js +1 -0
- package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/ui/assets/angular-html-CovppXt5.js +1 -0
- package/dist/ui/assets/angular-ts-0Y9l6YXu.js +1 -0
- package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
- package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
- package/dist/ui/assets/apl-BmBhiklA.js +1 -0
- package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
- package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
- package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/ui/assets/astro-BAmqcqyV.js +1 -0
- package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
- package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
- package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
- package/dist/ui/assets/berry-DKpUyyne.js +1 -0
- package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
- package/dist/ui/assets/blade-BjFcmRJo.js +1 -0
- package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
- package/dist/ui/assets/c-CaOJn4x0.js +1 -0
- package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
- package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
- package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
- package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
- package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
- package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
- package/dist/ui/assets/clarity-SemFz856.js +1 -0
- package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/ui/assets/cobol-DR4zWFMt.js +1 -0
- package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/ui/assets/coffee-ReGSThmM.js +1 -0
- package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
- package/dist/ui/assets/cpp-CnlQh6MO.js +1 -0
- package/dist/ui/assets/crystal-BrpPbH1U.js +1 -0
- package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
- package/dist/ui/assets/css-DRBEvyPU.js +1 -0
- package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
- package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/ui/assets/d-qD-0Kul2.js +1 -0
- package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/ui/assets/dart-DkHntEIa.js +1 -0
- package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
- package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/ui/assets/diff-woXpYk--.js +1 -0
- package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
- package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/ui/assets/edge-cfwpdvl9.js +1 -0
- package/dist/ui/assets/elixir-7owrJxb6.js +1 -0
- package/dist/ui/assets/elm-udiOjODk.js +1 -0
- package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
- package/dist/ui/assets/erb-8ctXHMkS.js +1 -0
- package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/ui/assets/fish-BJitypiv.js +1 -0
- package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
- package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
- package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
- package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
- package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/ui/assets/git-rebase-P0V_qK1w.js +1 -0
- package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/ui/assets/glimmer-js-CZ14ED7U.js +1 -0
- package/dist/ui/assets/glimmer-ts-DtBSFP-o.js +1 -0
- package/dist/ui/assets/glsl-BX2BkZJK.js +1 -0
- package/dist/ui/assets/gn-ilITqXS6.js +1 -0
- package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/ui/assets/go-BJwz_mda.js +1 -0
- package/dist/ui/assets/graphql-Dj_cPv4w.js +1 -0
- package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
- package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/ui/assets/hack-DGRkd8xn.js +1 -0
- package/dist/ui/assets/haml-BAPSvujW.js +1 -0
- package/dist/ui/assets/handlebars-PYksdULG.js +1 -0
- package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
- package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
- package/dist/ui/assets/hcl-Dh228itO.js +1 -0
- package/dist/ui/assets/heavy-payload-BjE0LSUi.js +4 -0
- package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
- package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
- package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
- package/dist/ui/assets/html-DloAPGZ1.js +1 -0
- package/dist/ui/assets/html-derivative-C6q86ZrS.js +1 -0
- package/dist/ui/assets/http-fPvYDvHA.js +1 -0
- package/dist/ui/assets/hurl-Dbir4WvE.js +1 -0
- package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
- package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
- package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
- package/dist/ui/assets/java-DvkqXg3z.js +1 -0
- package/dist/ui/assets/javascript-TONTV6Ty.js +1 -0
- package/dist/ui/assets/jinja-OM1F5Gct.js +1 -0
- package/dist/ui/assets/jison-CBttugts.js +1 -0
- package/dist/ui/assets/json-Db5oY-7J.js +1 -0
- package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
- package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/ui/assets/jsx-DKeJCgsc.js +1 -0
- package/dist/ui/assets/julia-NlIPhXaL.js +1 -0
- package/dist/ui/assets/just-BXBJB-mX.js +1 -0
- package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
- package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
- package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
- package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
- package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/ui/assets/latex-CGK-6yDy.js +1 -0
- package/dist/ui/assets/lean-CewbzKMR.js +1 -0
- package/dist/ui/assets/less-DVTAwKKz.js +1 -0
- package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/ui/assets/liquid-D7waQ0MY.js +1 -0
- package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
- package/dist/ui/assets/log-BNLmms1o.js +1 -0
- package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/ui/assets/lua-C_kkt4h1.js +1 -0
- package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
- package/dist/ui/assets/make-Dixweg8N.js +1 -0
- package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/ui/assets/marko-BokXuCts.js +1 -0
- package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/ui/assets/mdc-Bk0nnUJA.js +1 -0
- package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
- package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
- package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
- package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
- package/dist/ui/assets/move-B1IS1UjX.js +1 -0
- package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
- package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
- package/dist/ui/assets/nginx-jM6NF9TV.js +1 -0
- package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/ui/assets/nim-hYsL-WKb.js +1 -0
- package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
- package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
- package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
- package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/ui/assets/perl-DBI_cUHJ.js +1 -0
- package/dist/ui/assets/php-BX_aQhFl.js +1 -0
- package/dist/ui/assets/pierre-dark-sU4Zdns8.js +1 -0
- package/dist/ui/assets/pierre-dark-soft-HXgun5vs.js +1 -0
- package/dist/ui/assets/pierre-light-DWBk51d8.js +1 -0
- package/dist/ui/assets/pierre-light-soft-CodEzPxf.js +1 -0
- package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/ui/assets/po-BiJDBrnU.js +1 -0
- package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
- package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
- package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
- package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
- package/dist/ui/assets/pug-DwFAYy1e.js +1 -0
- package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/ui/assets/python-gzcpVVnB.js +1 -0
- package/dist/ui/assets/qml-eLSwQUKt.js +1 -0
- package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/ui/assets/r-BMnQsMLf.js +1 -0
- package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/ui/assets/raku-B3gFvitq.js +1 -0
- package/dist/ui/assets/razor-CgciiMyw.js +1 -0
- package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
- package/dist/ui/assets/reg-CRGYupPL.js +1 -0
- package/dist/ui/assets/regexp-ZrURNrn5.js +1 -0
- package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
- package/dist/ui/assets/review-payload-HGkpJjjm.js +1 -0
- package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
- package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
- package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
- package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
- package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/ui/assets/rst-EoAOr3M6.js +1 -0
- package/dist/ui/assets/ruby-Dqx5_Rqw.js +1 -0
- package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/ui/assets/sas-Bf6xPLSX.js +1 -0
- package/dist/ui/assets/sass-DXrisJhu.js +1 -0
- package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
- package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/ui/assets/scrollbar-DX4Ujotb.js +287 -0
- package/dist/ui/assets/scss-DDpW3yTd.js +1 -0
- package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/ui/assets/shellscript-DayAQ-W9.js +1 -0
- package/dist/ui/assets/shellsession-jPr_lSnA.js +1 -0
- package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
- package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
- package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
- package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
- package/dist/ui/assets/soy-X9adks8G.js +1 -0
- package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
- package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/ui/assets/sql-Bmt09AIR.js +1 -0
- package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/ui/assets/stata-DVkNpH8r.js +1 -0
- package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
- package/dist/ui/assets/surrealql-eEXzMb8j.js +1 -0
- package/dist/ui/assets/svelte-CFoBxZF3.js +1 -0
- package/dist/ui/assets/swift-DonLKvLd.js +1 -0
- package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
- package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
- package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
- package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/ui/assets/templ-Da_324JF.js +1 -0
- package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
- package/dist/ui/assets/tex-HAzlmKJ0.js +1 -0
- package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
- package/dist/ui/assets/ts-tags-7iLrNH8p.js +1 -0
- package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
- package/dist/ui/assets/tsx-gZaXf8PJ.js +1 -0
- package/dist/ui/assets/turtle-ByJddavk.js +1 -0
- package/dist/ui/assets/twig-CLpQ7p1N.js +1 -0
- package/dist/ui/assets/typescript-B2WWCRI2.js +1 -0
- package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
- package/dist/ui/assets/typst-DI99ib-x.js +1 -0
- package/dist/ui/assets/v-DETTlOr0.js +1 -0
- package/dist/ui/assets/vala-zf12oZj6.js +1 -0
- package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
- package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
- package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
- package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
- package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/ui/assets/vue-No_8Jygd.js +1 -0
- package/dist/ui/assets/vue-html-kHJg98Kk.js +1 -0
- package/dist/ui/assets/vue-vine-Df4cEYaN.js +1 -0
- package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
- package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/ui/assets/wit-DdvCle-K.js +1 -0
- package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/ui/assets/workspace-app-C_wsC6VL.js +117 -0
- package/dist/ui/assets/workspace-app-DHvE2ojW.css +1 -0
- package/dist/ui/assets/xml-DUZ05dpG.js +1 -0
- package/dist/ui/assets/xsl-Bwp3w2OF.js +1 -0
- package/dist/ui/assets/yaml-IA_-TEM1.js +1 -0
- package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
- package/dist/ui/workspace-app.html +15 -0
- package/dist/user-config.js +97 -0
- package/dist/workspace-store.js +162 -0
- package/dist/workspaces.js +528 -0
- package/docs/agent-profile-schema.md +119 -0
- package/docs/artifact-exchange.md +63 -0
- package/docs/chatgpt-coding-workflow.md +183 -0
- package/docs/configuration.md +197 -0
- package/docs/gotchas.md +243 -0
- package/docs/roadmap.md +195 -0
- package/docs/security.md +142 -0
- package/docs/setup.md +132 -0
- package/docs/versioning.md +189 -0
- package/examples/agents/claude-implementer.md +30 -0
- package/examples/agents/codex-explorer.md +27 -0
- package/examples/agents/codex-qa-tester.md +29 -0
- package/examples/agents/codex-worker.md +25 -0
- package/examples/agents/copilot-reviewer.md +25 -0
- package/examples/agents/cursor-agent-worker.md +26 -0
- package/examples/agents/opencode-explorer.md +27 -0
- package/examples/agents/pi-reviewer.md +27 -0
- package/package.json +102 -0
- package/scripts/dev-server.mjs +120 -0
- package/scripts/fix-node-pty-permissions.mjs +22 -0
- package/scripts/release-version.mjs +207 -0
- package/skills/subagent-delegation/SKILL.md +132 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const toolNames = {
|
|
2
|
+
openWorkspace: "open_workspace",
|
|
3
|
+
closeWorktree: "close_worktree",
|
|
4
|
+
read: "read",
|
|
5
|
+
write: "write",
|
|
6
|
+
edit: "edit",
|
|
7
|
+
grep: "grep",
|
|
8
|
+
glob: "glob",
|
|
9
|
+
ls: "ls",
|
|
10
|
+
shell: "bash",
|
|
11
|
+
};
|
|
12
|
+
export function buildServerInstructions(config, context = {}) {
|
|
13
|
+
return joinInstructions(capabilityContractInstructions(config, context), selectedWorkflowInstructions(config), config.appendInstructions);
|
|
14
|
+
}
|
|
15
|
+
export function buildToolDescriptions(config) {
|
|
16
|
+
const skillCapability = config.skillsEnabled
|
|
17
|
+
? " Advertised skill paths may be outside the workspace; only advertised SKILL.md files and files under already-loaded skill directories are readable."
|
|
18
|
+
: "";
|
|
19
|
+
const shellSurface = config.toolMode === "minimal"
|
|
20
|
+
? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
|
|
21
|
+
: "";
|
|
22
|
+
return {
|
|
23
|
+
read: `Read a file inside an open workspace. Instruction files returned by ${toolNames.openWorkspace} and advertised skill files are also readable when applicable.${skillCapability} Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
24
|
+
write: `Create or completely overwrite a file inside an open workspace. The path is resolved within the workspace filesystem boundary. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
25
|
+
edit: `Edit one file inside an open workspace by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
26
|
+
applyPatch: `Apply one Codex-style patch inside an open workspace. Supports adding, overwriting, updating, deleting, and moving files. Paths must be relative to the workspace. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
27
|
+
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. Do not use ${toolNames.shell} to create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes. Call ${toolNames.openWorkspace} first and pass workspaceId. This capability should only be exposed behind strong authentication.`,
|
|
28
|
+
shellCommand: "Shell command to run with the local user's authority.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function capabilityContractInstructions(config, context) {
|
|
32
|
+
const workspaceLifecycle = config.toolMode === "codex"
|
|
33
|
+
? `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout and reuse its workspaceId. Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches, not detached HEADs. When work in a managed worktree is complete and verified, call ${toolNames.closeWorktree}; it commits remaining worktree changes, fast-forwards the original target branch only when safe, then removes the worktree and its branch. If the target branch diverged or the source checkout is dirty, closing is refused and the worktree is preserved.`
|
|
34
|
+
: `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout and reuse its workspaceId for later file, search, edit, write, show-changes, and shell tools. Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches, not detached HEADs. When work in a managed worktree is complete and verified, call ${toolNames.closeWorktree}; it commits remaining worktree changes, fast-forwards the original target branch only when safe, then removes the worktree and its branch. If the target branch diverged or the source checkout is dirty, closing is refused and the worktree is preserved.`;
|
|
35
|
+
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it.`;
|
|
36
|
+
const skills = config.skillsEnabled
|
|
37
|
+
? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories.`
|
|
38
|
+
: "";
|
|
39
|
+
const toolSurface = toolSurfaceInstructions(config);
|
|
40
|
+
const artifact = config.artifactsEnabled && context.artifactDownloadSupported
|
|
41
|
+
? "When the user supplies or generates a file that is not present on the ForgeRelay host, use download_artifact with its native file value, the existing workspace ID, and a suitable relative destination path chosen from the user's request and project structure. The tool refuses to overwrite an existing destination and returns the normalized workspace-relative path. Use normal workspace tools when explicit inspection, replacement, movement, renaming, or deletion is needed. Do not recreate binary files with write/edit calls or place signed URLs, native file objects, base64 content, or invented host paths in shell commands or logs."
|
|
42
|
+
: "";
|
|
43
|
+
const showChanges = config.widgets === "changes"
|
|
44
|
+
? "If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs."
|
|
45
|
+
: "";
|
|
46
|
+
return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, artifact, showChanges);
|
|
47
|
+
}
|
|
48
|
+
function toolSurfaceInstructions(config) {
|
|
49
|
+
if (config.toolMode === "codex") {
|
|
50
|
+
return `In codex tool mode, workspace file and command operations use ${toolNames.read}, apply_patch, exec_command, and write_stdin.`;
|
|
51
|
+
}
|
|
52
|
+
if (config.toolMode === "full") {
|
|
53
|
+
return `In full tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are available alongside the core workspace tools.`;
|
|
54
|
+
}
|
|
55
|
+
return `In minimal tool mode, dedicated ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} inspection tools are disabled; the core workspace tools remain available.`;
|
|
56
|
+
}
|
|
57
|
+
function selectedWorkflowInstructions(config) {
|
|
58
|
+
if (config.workflowInstructions === false)
|
|
59
|
+
return "";
|
|
60
|
+
if (typeof config.workflowInstructions === "string")
|
|
61
|
+
return config.workflowInstructions;
|
|
62
|
+
return defaultWorkflowInstructions(config);
|
|
63
|
+
}
|
|
64
|
+
function defaultWorkflowInstructions(config) {
|
|
65
|
+
if (config.toolMode === "codex") {
|
|
66
|
+
return `Use ${toolNames.read} for direct file reads, apply_patch for all file modifications, exec_command for inspection, tests, builds, and other commands, and write_stdin to poll or interact with running processes.`;
|
|
67
|
+
}
|
|
68
|
+
const inspection = config.toolMode === "full"
|
|
69
|
+
? `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection.`
|
|
70
|
+
: `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
71
|
+
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell.`, `Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.`);
|
|
72
|
+
}
|
|
73
|
+
function joinInstructions(...parts) {
|
|
74
|
+
return parts
|
|
75
|
+
.map((part) => part?.trim())
|
|
76
|
+
.filter((part) => Boolean(part))
|
|
77
|
+
.join(" ");
|
|
78
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class McpSessionRegistry {
|
|
2
|
+
sessions = new Map();
|
|
3
|
+
now;
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.now = options.now ?? Date.now;
|
|
6
|
+
}
|
|
7
|
+
get size() {
|
|
8
|
+
return this.sessions.size;
|
|
9
|
+
}
|
|
10
|
+
register(sessionId, transport) {
|
|
11
|
+
this.sessions.set(sessionId, {
|
|
12
|
+
transport,
|
|
13
|
+
lastActivityAt: this.now(),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
get(sessionId) {
|
|
17
|
+
const entry = this.sessions.get(sessionId);
|
|
18
|
+
if (!entry)
|
|
19
|
+
return undefined;
|
|
20
|
+
entry.lastActivityAt = this.now();
|
|
21
|
+
return entry.transport;
|
|
22
|
+
}
|
|
23
|
+
remove(sessionId) {
|
|
24
|
+
return this.sessions.delete(sessionId);
|
|
25
|
+
}
|
|
26
|
+
async closeIdle(idleTimeoutMs) {
|
|
27
|
+
const cutoff = this.now() - idleTimeoutMs;
|
|
28
|
+
const idleSessions = [];
|
|
29
|
+
for (const [sessionId, entry] of this.sessions) {
|
|
30
|
+
if (entry.lastActivityAt > cutoff)
|
|
31
|
+
continue;
|
|
32
|
+
this.sessions.delete(sessionId);
|
|
33
|
+
idleSessions.push({ sessionId, transport: entry.transport });
|
|
34
|
+
}
|
|
35
|
+
return closeSessions(idleSessions);
|
|
36
|
+
}
|
|
37
|
+
async closeAll() {
|
|
38
|
+
const sessions = Array.from(this.sessions, ([sessionId, entry]) => ({
|
|
39
|
+
sessionId,
|
|
40
|
+
transport: entry.transport,
|
|
41
|
+
}));
|
|
42
|
+
this.sessions.clear();
|
|
43
|
+
return closeSessions(sessions);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function closeSessions(sessions) {
|
|
47
|
+
return Promise.all(sessions.map(async ({ sessionId, transport }) => {
|
|
48
|
+
try {
|
|
49
|
+
await transport.close();
|
|
50
|
+
return { sessionId };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return { sessionId, error };
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { timingSafeEqual, randomBytes, randomUUID, createHash } from "node:crypto";
|
|
2
|
+
import { AccessDeniedError, InvalidGrantError, InvalidRequestError, InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
|
+
import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
|
|
4
|
+
import { SqliteOAuthClientsStore, SqliteOAuthStore } from "./oauth-store.js";
|
|
5
|
+
const CODE_TTL_MS = 5 * 60 * 1000;
|
|
6
|
+
function randomToken() {
|
|
7
|
+
return randomBytes(32).toString("base64url");
|
|
8
|
+
}
|
|
9
|
+
function safeEquals(a, b) {
|
|
10
|
+
const left = Buffer.from(a);
|
|
11
|
+
const right = Buffer.from(b);
|
|
12
|
+
if (left.byteLength !== right.byteLength)
|
|
13
|
+
return false;
|
|
14
|
+
return timingSafeEqual(left, right);
|
|
15
|
+
}
|
|
16
|
+
function htmlEscape(value) {
|
|
17
|
+
return value
|
|
18
|
+
.replaceAll("&", "&")
|
|
19
|
+
.replaceAll("<", "<")
|
|
20
|
+
.replaceAll(">", ">")
|
|
21
|
+
.replaceAll('"', """)
|
|
22
|
+
.replaceAll("'", "'");
|
|
23
|
+
}
|
|
24
|
+
function formHtml(params) {
|
|
25
|
+
const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "devspace";
|
|
26
|
+
const resourceText = params.resource?.href ?? "ForgeRelay MCP endpoint";
|
|
27
|
+
const error = params.error
|
|
28
|
+
? `<p class="error">${htmlEscape(params.error)}</p>`
|
|
29
|
+
: "";
|
|
30
|
+
const hiddenFields = Object.entries(params.fields)
|
|
31
|
+
.filter((entry) => entry[1] !== undefined)
|
|
32
|
+
.map(([name, value]) => ` <input type="hidden" name="${htmlEscape(name)}" value="${htmlEscape(value)}" />`)
|
|
33
|
+
.join("\n");
|
|
34
|
+
return `<!doctype html>
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<head>
|
|
37
|
+
<meta charset="utf-8" />
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
39
|
+
<title>Connect ForgeRelay</title>
|
|
40
|
+
<style>
|
|
41
|
+
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
|
|
42
|
+
main { max-width: 440px; margin: 12vh auto; padding: 32px; background: #111827; border: 1px solid #334155; border-radius: 18px; box-shadow: 0 24px 80px rgba(0,0,0,.35); }
|
|
43
|
+
h1 { margin: 0 0 12px; font-size: 28px; }
|
|
44
|
+
p { line-height: 1.5; color: #cbd5e1; }
|
|
45
|
+
dl { padding: 16px; background: #020617; border-radius: 12px; }
|
|
46
|
+
dt { color: #94a3b8; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
|
|
47
|
+
dd { margin: 4px 0 12px; word-break: break-word; }
|
|
48
|
+
label { display: block; margin: 18px 0 8px; font-weight: 600; }
|
|
49
|
+
input { box-sizing: border-box; width: 100%; padding: 12px 14px; border-radius: 10px; border: 1px solid #475569; background: #020617; color: #e2e8f0; font-size: 16px; }
|
|
50
|
+
button { margin-top: 18px; width: 100%; border: 0; border-radius: 10px; padding: 12px 14px; font-weight: 700; color: #020617; background: #38bdf8; cursor: pointer; }
|
|
51
|
+
.error { color: #fecaca; background: #7f1d1d; border-radius: 10px; padding: 10px 12px; }
|
|
52
|
+
.warning { color: #fde68a; }
|
|
53
|
+
</style>
|
|
54
|
+
</head>
|
|
55
|
+
<body>
|
|
56
|
+
<main>
|
|
57
|
+
<h1>Connect ForgeRelay</h1>
|
|
58
|
+
<p class="warning">Only approve this if you are intentionally connecting your own ChatGPT or MCP client to this local machine.</p>
|
|
59
|
+
${error}
|
|
60
|
+
<dl>
|
|
61
|
+
<dt>Client</dt><dd>${htmlEscape(params.clientName)}</dd>
|
|
62
|
+
<dt>Scope</dt><dd>${htmlEscape(scopeText)}</dd>
|
|
63
|
+
<dt>Resource</dt><dd>${htmlEscape(resourceText)}</dd>
|
|
64
|
+
</dl>
|
|
65
|
+
<form method="post">
|
|
66
|
+
${hiddenFields}
|
|
67
|
+
<label for="owner_token">Owner password</label>
|
|
68
|
+
<input id="owner_token" name="owner_token" type="password" autocomplete="current-password" autofocus required />
|
|
69
|
+
<button type="submit">Authorize ForgeRelay</button>
|
|
70
|
+
</form>
|
|
71
|
+
</main>
|
|
72
|
+
</body>
|
|
73
|
+
</html>`;
|
|
74
|
+
}
|
|
75
|
+
function requestedScopesAllowed(requested, supported) {
|
|
76
|
+
return requested.every((scope) => supported.includes(scope));
|
|
77
|
+
}
|
|
78
|
+
export class SingleUserOAuthProvider {
|
|
79
|
+
config;
|
|
80
|
+
clientsStore;
|
|
81
|
+
codes = new Map();
|
|
82
|
+
oauthStore;
|
|
83
|
+
resourceServerUrl;
|
|
84
|
+
constructor(config, resourceServerUrl, stateDir) {
|
|
85
|
+
this.config = config;
|
|
86
|
+
this.resourceServerUrl = resourceUrlFromServerUrl(resourceServerUrl);
|
|
87
|
+
this.oauthStore = new SqliteOAuthStore(stateDir);
|
|
88
|
+
this.clientsStore = new SqliteOAuthClientsStore(this.oauthStore, config.allowedRedirectHosts);
|
|
89
|
+
}
|
|
90
|
+
async authorize(client, params, res) {
|
|
91
|
+
if (!params.resource || !checkResourceAllowed({ requestedResource: params.resource, configuredResource: this.resourceServerUrl })) {
|
|
92
|
+
throw new InvalidRequestError("Invalid or missing OAuth resource");
|
|
93
|
+
}
|
|
94
|
+
if (!requestedScopesAllowed(params.scopes ?? [], this.config.scopes)) {
|
|
95
|
+
throw new InvalidRequestError("Requested scope is not supported");
|
|
96
|
+
}
|
|
97
|
+
if (res.req.method !== "POST") {
|
|
98
|
+
res.status(200).setHeader("Content-Type", "text/html; charset=utf-8");
|
|
99
|
+
res.send(formHtml({
|
|
100
|
+
clientName: client.client_name ?? client.client_id,
|
|
101
|
+
scopes: params.scopes ?? this.config.scopes,
|
|
102
|
+
resource: params.resource,
|
|
103
|
+
fields: authorizationFormFields(client, params),
|
|
104
|
+
}));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const providedToken = String(res.req.body?.owner_token ?? "");
|
|
108
|
+
if (!safeEquals(providedToken, this.config.ownerToken)) {
|
|
109
|
+
res.status(401).setHeader("Content-Type", "text/html; charset=utf-8");
|
|
110
|
+
res.send(formHtml({
|
|
111
|
+
error: "The Owner password was not accepted.",
|
|
112
|
+
clientName: client.client_name ?? client.client_id,
|
|
113
|
+
scopes: params.scopes ?? this.config.scopes,
|
|
114
|
+
resource: params.resource,
|
|
115
|
+
fields: authorizationFormFields(client, params),
|
|
116
|
+
}));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const code = `code-${randomUUID()}`;
|
|
120
|
+
this.codes.set(code, {
|
|
121
|
+
clientId: client.client_id,
|
|
122
|
+
params,
|
|
123
|
+
expiresAtMs: Date.now() + CODE_TTL_MS,
|
|
124
|
+
});
|
|
125
|
+
const redirectUrl = new URL(params.redirectUri);
|
|
126
|
+
redirectUrl.searchParams.set("code", code);
|
|
127
|
+
if (params.state !== undefined)
|
|
128
|
+
redirectUrl.searchParams.set("state", params.state);
|
|
129
|
+
res.redirect(302, redirectUrl.href);
|
|
130
|
+
}
|
|
131
|
+
async challengeForAuthorizationCode(client, authorizationCode) {
|
|
132
|
+
const record = this.validCodeRecord(client, authorizationCode);
|
|
133
|
+
return record.params.codeChallenge;
|
|
134
|
+
}
|
|
135
|
+
async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, redirectUri, resource) {
|
|
136
|
+
const record = this.validCodeRecord(client, authorizationCode);
|
|
137
|
+
if (redirectUri && redirectUri !== record.params.redirectUri) {
|
|
138
|
+
throw new InvalidGrantError("redirect_uri does not match the authorization request");
|
|
139
|
+
}
|
|
140
|
+
if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
|
|
141
|
+
throw new InvalidGrantError("Invalid resource");
|
|
142
|
+
}
|
|
143
|
+
this.codes.delete(authorizationCode);
|
|
144
|
+
return this.issueTokens(client.client_id, record.params.scopes ?? this.config.scopes, record.params.resource);
|
|
145
|
+
}
|
|
146
|
+
async exchangeRefreshToken(client, refreshToken, scopes, resource) {
|
|
147
|
+
const refreshTokenHash = hashToken(refreshToken);
|
|
148
|
+
const record = this.oauthStore.getRefreshToken(refreshTokenHash);
|
|
149
|
+
if (!record || record.clientId !== client.client_id || record.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
150
|
+
throw new InvalidGrantError("Invalid refresh token");
|
|
151
|
+
}
|
|
152
|
+
if (resource && !checkResourceAllowed({ requestedResource: resource, configuredResource: this.resourceServerUrl })) {
|
|
153
|
+
throw new InvalidGrantError("Invalid resource");
|
|
154
|
+
}
|
|
155
|
+
const requestedScopes = scopes ?? record.scopes;
|
|
156
|
+
if (!requestedScopes.every((scope) => record.scopes.includes(scope))) {
|
|
157
|
+
throw new AccessDeniedError("Refresh token cannot grant requested scopes");
|
|
158
|
+
}
|
|
159
|
+
return this.issueTokens(client.client_id, requestedScopes, resource ?? (record.resource ? new URL(record.resource) : undefined), refreshTokenHash);
|
|
160
|
+
}
|
|
161
|
+
async verifyAccessToken(token) {
|
|
162
|
+
const record = this.oauthStore.getAccessToken(hashToken(token));
|
|
163
|
+
if (!record || record.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
164
|
+
throw new InvalidTokenError("Invalid or expired access token");
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
token,
|
|
168
|
+
clientId: record.clientId,
|
|
169
|
+
scopes: record.scopes,
|
|
170
|
+
expiresAt: record.expiresAt,
|
|
171
|
+
resource: record.resource ? new URL(record.resource) : undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async revokeToken(_client, request) {
|
|
175
|
+
const hashed = hashToken(request.token);
|
|
176
|
+
this.oauthStore.deleteAccessToken(hashed);
|
|
177
|
+
this.oauthStore.deleteRefreshToken(hashed);
|
|
178
|
+
}
|
|
179
|
+
close() {
|
|
180
|
+
this.oauthStore.close();
|
|
181
|
+
}
|
|
182
|
+
validCodeRecord(client, authorizationCode) {
|
|
183
|
+
const record = this.codes.get(authorizationCode);
|
|
184
|
+
if (!record || record.clientId !== client.client_id || record.expiresAtMs < Date.now()) {
|
|
185
|
+
throw new InvalidGrantError("Invalid authorization code");
|
|
186
|
+
}
|
|
187
|
+
return record;
|
|
188
|
+
}
|
|
189
|
+
issueTokens(clientId, scopes, resource, consumedRefreshTokenHash) {
|
|
190
|
+
const now = Math.floor(Date.now() / 1000);
|
|
191
|
+
const accessToken = randomToken();
|
|
192
|
+
const refreshToken = randomToken();
|
|
193
|
+
const accessExpiresAt = now + this.config.accessTokenTtlSeconds;
|
|
194
|
+
const refreshExpiresAt = now + this.config.refreshTokenTtlSeconds;
|
|
195
|
+
const saved = this.oauthStore.saveTokenPair({
|
|
196
|
+
accessTokenHash: hashToken(accessToken),
|
|
197
|
+
accessToken: {
|
|
198
|
+
clientId,
|
|
199
|
+
scopes,
|
|
200
|
+
expiresAt: accessExpiresAt,
|
|
201
|
+
resource: resource?.href,
|
|
202
|
+
},
|
|
203
|
+
refreshTokenHash: hashToken(refreshToken),
|
|
204
|
+
refreshToken: {
|
|
205
|
+
clientId,
|
|
206
|
+
scopes,
|
|
207
|
+
expiresAt: refreshExpiresAt,
|
|
208
|
+
resource: resource?.href,
|
|
209
|
+
},
|
|
210
|
+
}, consumedRefreshTokenHash);
|
|
211
|
+
if (!saved) {
|
|
212
|
+
throw new InvalidGrantError("Invalid refresh token");
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
access_token: accessToken,
|
|
216
|
+
token_type: "bearer",
|
|
217
|
+
expires_in: this.config.accessTokenTtlSeconds,
|
|
218
|
+
refresh_token: refreshToken,
|
|
219
|
+
scope: scopes.join(" "),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function authorizationFormFields(client, params) {
|
|
224
|
+
return {
|
|
225
|
+
response_type: "code",
|
|
226
|
+
client_id: client.client_id,
|
|
227
|
+
redirect_uri: params.redirectUri,
|
|
228
|
+
code_challenge: params.codeChallenge,
|
|
229
|
+
code_challenge_method: "S256",
|
|
230
|
+
scope: params.scopes?.join(" "),
|
|
231
|
+
state: params.state,
|
|
232
|
+
resource: params.resource?.href,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function hashToken(token) {
|
|
236
|
+
return createHash("sha256").update(token).digest("base64url");
|
|
237
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
|
+
import { openDatabase } from "./db/client.js";
|
|
4
|
+
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = new URL(redirectUri);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))
|
|
13
|
+
return true;
|
|
14
|
+
return allowedHosts.includes(parsed.hostname);
|
|
15
|
+
}
|
|
16
|
+
export class SqliteOAuthStore {
|
|
17
|
+
database;
|
|
18
|
+
constructor(stateDir) {
|
|
19
|
+
this.database = openDatabase(stateDir);
|
|
20
|
+
this.deleteExpiredTokens(Math.floor(Date.now() / 1000));
|
|
21
|
+
}
|
|
22
|
+
getClient(clientId) {
|
|
23
|
+
const row = this.database.sqlite
|
|
24
|
+
.prepare("select client_json from oauth_clients where client_id = ?")
|
|
25
|
+
.get(clientId);
|
|
26
|
+
return row ? JSON.parse(row.client_json) : undefined;
|
|
27
|
+
}
|
|
28
|
+
registerClient(client, allowedRedirectHosts) {
|
|
29
|
+
if (!client.redirect_uris.every((uri) => redirectHostAllowed(String(uri), allowedRedirectHosts))) {
|
|
30
|
+
throw new InvalidRequestError("Client redirect_uri is not allowed for this ForgeRelay server");
|
|
31
|
+
}
|
|
32
|
+
const now = Math.floor(Date.now() / 1000);
|
|
33
|
+
const registered = {
|
|
34
|
+
...client,
|
|
35
|
+
client_id: `forgerelay-${randomUUID()}`,
|
|
36
|
+
client_id_issued_at: now,
|
|
37
|
+
token_endpoint_auth_method: client.token_endpoint_auth_method ?? "none",
|
|
38
|
+
grant_types: client.grant_types ?? ["authorization_code", "refresh_token"],
|
|
39
|
+
response_types: client.response_types ?? ["code"],
|
|
40
|
+
};
|
|
41
|
+
this.database.sqlite
|
|
42
|
+
.prepare("insert into oauth_clients (client_id, client_json, issued_at) values (?, ?, ?)")
|
|
43
|
+
.run(registered.client_id, JSON.stringify(registered), now);
|
|
44
|
+
return registered;
|
|
45
|
+
}
|
|
46
|
+
saveAccessToken(tokenHash, record) {
|
|
47
|
+
this.database.sqlite
|
|
48
|
+
.prepare(`insert into oauth_access_tokens (token_hash, client_id, scopes_json, expires_at, resource)
|
|
49
|
+
values (?, ?, ?, ?, ?)
|
|
50
|
+
on conflict(token_hash) do update set
|
|
51
|
+
client_id = excluded.client_id,
|
|
52
|
+
scopes_json = excluded.scopes_json,
|
|
53
|
+
expires_at = excluded.expires_at,
|
|
54
|
+
resource = excluded.resource`)
|
|
55
|
+
.run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
|
|
56
|
+
}
|
|
57
|
+
getAccessToken(tokenHash) {
|
|
58
|
+
const row = this.database.sqlite
|
|
59
|
+
.prepare("select client_id, scopes_json, expires_at, resource from oauth_access_tokens where token_hash = ?")
|
|
60
|
+
.get(tokenHash);
|
|
61
|
+
return row ? rowToAccessTokenRecord(row) : undefined;
|
|
62
|
+
}
|
|
63
|
+
deleteAccessToken(tokenHash) {
|
|
64
|
+
this.database.sqlite.prepare("delete from oauth_access_tokens where token_hash = ?").run(tokenHash);
|
|
65
|
+
}
|
|
66
|
+
saveRefreshToken(tokenHash, record) {
|
|
67
|
+
this.database.sqlite
|
|
68
|
+
.prepare(`insert into oauth_refresh_tokens (token_hash, client_id, scopes_json, expires_at, resource)
|
|
69
|
+
values (?, ?, ?, ?, ?)
|
|
70
|
+
on conflict(token_hash) do update set
|
|
71
|
+
client_id = excluded.client_id,
|
|
72
|
+
scopes_json = excluded.scopes_json,
|
|
73
|
+
expires_at = excluded.expires_at,
|
|
74
|
+
resource = excluded.resource`)
|
|
75
|
+
.run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
|
|
76
|
+
}
|
|
77
|
+
saveTokenPair(pair, consumedRefreshTokenHash) {
|
|
78
|
+
const save = this.database.sqlite.transaction(() => {
|
|
79
|
+
if (consumedRefreshTokenHash) {
|
|
80
|
+
const result = this.database.sqlite
|
|
81
|
+
.prepare("delete from oauth_refresh_tokens where token_hash = ?")
|
|
82
|
+
.run(consumedRefreshTokenHash);
|
|
83
|
+
if (result.changes !== 1)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
this.saveAccessToken(pair.accessTokenHash, pair.accessToken);
|
|
87
|
+
this.saveRefreshToken(pair.refreshTokenHash, pair.refreshToken);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
return save.immediate();
|
|
91
|
+
}
|
|
92
|
+
getRefreshToken(tokenHash) {
|
|
93
|
+
const row = this.database.sqlite
|
|
94
|
+
.prepare("select client_id, scopes_json, expires_at, resource from oauth_refresh_tokens where token_hash = ?")
|
|
95
|
+
.get(tokenHash);
|
|
96
|
+
return row ? rowToRefreshTokenRecord(row) : undefined;
|
|
97
|
+
}
|
|
98
|
+
deleteRefreshToken(tokenHash) {
|
|
99
|
+
this.database.sqlite.prepare("delete from oauth_refresh_tokens where token_hash = ?").run(tokenHash);
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.database.close();
|
|
103
|
+
}
|
|
104
|
+
deleteExpiredTokens(nowSeconds) {
|
|
105
|
+
this.database.sqlite.prepare("delete from oauth_access_tokens where expires_at < ?").run(nowSeconds);
|
|
106
|
+
this.database.sqlite.prepare("delete from oauth_refresh_tokens where expires_at < ?").run(nowSeconds);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export class SqliteOAuthClientsStore {
|
|
110
|
+
store;
|
|
111
|
+
allowedRedirectHosts;
|
|
112
|
+
constructor(store, allowedRedirectHosts) {
|
|
113
|
+
this.store = store;
|
|
114
|
+
this.allowedRedirectHosts = allowedRedirectHosts;
|
|
115
|
+
}
|
|
116
|
+
getClient(clientId) {
|
|
117
|
+
return this.store.getClient(clientId);
|
|
118
|
+
}
|
|
119
|
+
registerClient(client) {
|
|
120
|
+
return this.store.registerClient(client, this.allowedRedirectHosts);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function rowToAccessTokenRecord(row) {
|
|
124
|
+
return {
|
|
125
|
+
clientId: row.client_id,
|
|
126
|
+
scopes: JSON.parse(row.scopes_json),
|
|
127
|
+
expiresAt: row.expires_at,
|
|
128
|
+
resource: row.resource ?? undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function rowToRefreshTokenRecord(row) {
|
|
132
|
+
return {
|
|
133
|
+
clientId: row.client_id,
|
|
134
|
+
scopes: JSON.parse(row.scopes_json),
|
|
135
|
+
expiresAt: row.expires_at,
|
|
136
|
+
resource: row.resource ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
package/dist/pi-tools.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolveAllowedPath } from "./roots.js";
|
|
3
|
+
function toMcpContent(result) {
|
|
4
|
+
return result.content.map((content) => {
|
|
5
|
+
if (content.type === "text") {
|
|
6
|
+
return { type: "text", text: content.text };
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
type: "image",
|
|
10
|
+
data: content.data,
|
|
11
|
+
mimeType: content.mimeType,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function formatToolError(error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return [{ type: "text", text: message }];
|
|
18
|
+
}
|
|
19
|
+
async function runTool(execute, input, context) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await execute(input);
|
|
22
|
+
return {
|
|
23
|
+
content: toMcpContent(result),
|
|
24
|
+
details: result.details,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return { content: formatToolError(error), isError: true };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function readFileTool(input, context) {
|
|
32
|
+
const path = resolveAllowedPath(input.path, context.cwd, context.readRoots ?? [context.root]);
|
|
33
|
+
const tool = createReadTool(context.cwd);
|
|
34
|
+
return runTool((params) => tool.execute("read_file", params), {
|
|
35
|
+
path,
|
|
36
|
+
offset: input.offset,
|
|
37
|
+
limit: input.limit,
|
|
38
|
+
}, context);
|
|
39
|
+
}
|
|
40
|
+
export async function writeFileTool(input, context) {
|
|
41
|
+
const path = resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
42
|
+
const tool = createWriteTool(context.cwd);
|
|
43
|
+
return runTool((params) => tool.execute("write_file", params), {
|
|
44
|
+
path,
|
|
45
|
+
content: input.content,
|
|
46
|
+
}, context);
|
|
47
|
+
}
|
|
48
|
+
export async function editFileTool(input, context) {
|
|
49
|
+
const path = resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
50
|
+
const tool = createEditTool(context.cwd);
|
|
51
|
+
return runTool((params) => tool.execute("edit_file", params), {
|
|
52
|
+
path,
|
|
53
|
+
edits: input.edits,
|
|
54
|
+
}, context);
|
|
55
|
+
}
|
|
56
|
+
export async function grepFilesTool(input, context) {
|
|
57
|
+
if (input.path)
|
|
58
|
+
resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
59
|
+
const tool = createGrepTool(context.cwd);
|
|
60
|
+
return runTool((params) => tool.execute("grep_files", params), input, context);
|
|
61
|
+
}
|
|
62
|
+
export async function findFilesTool(input, context) {
|
|
63
|
+
if (input.path)
|
|
64
|
+
resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
65
|
+
const tool = createFindTool(context.cwd);
|
|
66
|
+
return runTool((params) => tool.execute("find_files", params), input, context);
|
|
67
|
+
}
|
|
68
|
+
export async function listDirectoryTool(input, context) {
|
|
69
|
+
if (input.path)
|
|
70
|
+
resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
71
|
+
const tool = createLsTool(context.cwd);
|
|
72
|
+
return runTool((params) => tool.execute("list_directory", params), input, context);
|
|
73
|
+
}
|
|
74
|
+
export async function runShellTool(input, context) {
|
|
75
|
+
const tool = createBashTool(context.cwd);
|
|
76
|
+
const timeout = input.timeout === undefined ? 30 : Math.min(input.timeout, 300);
|
|
77
|
+
return runTool((params) => tool.execute("run_shell", params), {
|
|
78
|
+
command: input.command,
|
|
79
|
+
timeout,
|
|
80
|
+
}, context);
|
|
81
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
const defaultProcessTreeRuntime = {
|
|
4
|
+
platform: process.platform,
|
|
5
|
+
killGroup: (pid, signal) => process.kill(-pid, signal),
|
|
6
|
+
killWindowsTree: (pid) => {
|
|
7
|
+
const result = spawnSync("taskkill.exe", ["/pid", String(pid), "/T", "/F"], {
|
|
8
|
+
stdio: "ignore",
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
});
|
|
11
|
+
return !result.error && result.status === 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const LOGIN_SHELLS = new Set(["bash", "ksh", "zsh"]);
|
|
15
|
+
const POSIX_SHELLS = new Set(["ash", "dash", "sh"]);
|
|
16
|
+
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
return {
|
|
19
|
+
executable: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
|
|
20
|
+
args: ["/d", "/s", "/c", command],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const configuredShell = environment.SHELL;
|
|
24
|
+
const shellName = configuredShell ? basename(configuredShell) : "";
|
|
25
|
+
if (configuredShell && LOGIN_SHELLS.has(shellName)) {
|
|
26
|
+
return { executable: configuredShell, args: ["-lc", command] };
|
|
27
|
+
}
|
|
28
|
+
if (configuredShell && POSIX_SHELLS.has(shellName)) {
|
|
29
|
+
return { executable: configuredShell, args: ["-c", command] };
|
|
30
|
+
}
|
|
31
|
+
return { executable: "/bin/sh", args: ["-c", command] };
|
|
32
|
+
}
|
|
33
|
+
export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
|
|
34
|
+
if (runtime.platform === "win32" && child.pid) {
|
|
35
|
+
if (runtime.killWindowsTree(child.pid))
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
else if (detached && child.pid) {
|
|
39
|
+
try {
|
|
40
|
+
runtime.killGroup(child.pid, signal);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ESRCH")
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
child.kill(signal);
|
|
49
|
+
}
|