@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,255 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { mkdir, realpath, rm, stat } from "node:fs/promises";
|
|
5
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
6
|
+
import { assertAllowedPath, isPathInsideRoot } from "./roots.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export class GitWorktreeError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "GitWorktreeError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function createManagedWorktree(input) {
|
|
17
|
+
const sourcePath = assertAllowedPath(input.sourcePath, input.config.allowedRoots);
|
|
18
|
+
try {
|
|
19
|
+
const sourceStats = await stat(sourcePath);
|
|
20
|
+
if (!sourceStats.isDirectory()) {
|
|
21
|
+
throw new GitWorktreeError("GIT_REPOSITORY_NOT_FOUND", `Cannot open workspace in worktree mode because the source path is not a directory: ${input.sourcePath}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error instanceof GitWorktreeError)
|
|
26
|
+
throw error;
|
|
27
|
+
throw new GitWorktreeError("GIT_REPOSITORY_NOT_FOUND", `Cannot open workspace in worktree mode because the source path does not exist: ${input.sourcePath}`);
|
|
28
|
+
}
|
|
29
|
+
const { sourceRoot, baseRef, baseSha, targetBranch } = await resolveManagedWorktreeBase(input);
|
|
30
|
+
const dirtySource = (await git(["status", "--porcelain=v1"], sourceRoot)).trim().length > 0;
|
|
31
|
+
const worktreeId = randomBytes(4).toString("hex");
|
|
32
|
+
const worktreePath = managedWorktreePath({
|
|
33
|
+
worktreeRoot: input.config.worktreeRoot,
|
|
34
|
+
repoRoot: sourceRoot,
|
|
35
|
+
worktreeId,
|
|
36
|
+
});
|
|
37
|
+
const branch = managedWorktreeBranch({ repoRoot: sourceRoot, worktreeId });
|
|
38
|
+
await mkdir(input.config.worktreeRoot, { recursive: true });
|
|
39
|
+
assertAllowedPath(worktreePath, [input.config.worktreeRoot]);
|
|
40
|
+
try {
|
|
41
|
+
await git(["worktree", "add", "-b", branch, worktreePath, baseSha], sourceRoot);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
await rm(worktreePath, { recursive: true, force: true });
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
throw new GitWorktreeError("GIT_WORKTREE_CREATE_FAILED", `Git failed to create the managed worktree. ${message}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
sourceRoot,
|
|
50
|
+
path: worktreePath,
|
|
51
|
+
baseRef,
|
|
52
|
+
baseSha,
|
|
53
|
+
branch,
|
|
54
|
+
targetBranch,
|
|
55
|
+
dirtySource,
|
|
56
|
+
detached: false,
|
|
57
|
+
managed: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export async function closeManagedWorktree(input) {
|
|
61
|
+
const sourceRoot = assertAllowedPath(input.worktree.sourceRoot, input.config.allowedRoots);
|
|
62
|
+
const worktreePath = assertAllowedPath(input.worktree.path, [input.config.worktreeRoot]);
|
|
63
|
+
const sourceBranch = await currentBranch(sourceRoot);
|
|
64
|
+
if (sourceBranch !== input.worktree.targetBranch) {
|
|
65
|
+
throw new GitWorktreeError("GIT_WORKTREE_SOURCE_BRANCH_CHANGED", `Cannot close worktree because the source checkout is on branch ${JSON.stringify(sourceBranch)} instead of target branch ${JSON.stringify(input.worktree.targetBranch)}. Switch the source checkout back to the target branch and retry.`);
|
|
66
|
+
}
|
|
67
|
+
const sourceDirty = (await git(["status", "--porcelain=v1"], sourceRoot)).trim().length > 0;
|
|
68
|
+
if (sourceDirty) {
|
|
69
|
+
throw new GitWorktreeError("GIT_WORKTREE_SOURCE_DIRTY", "Cannot close worktree because the source checkout has uncommitted changes. Commit or stash them first; the managed worktree is unchanged.");
|
|
70
|
+
}
|
|
71
|
+
const worktreeBranch = await currentBranch(worktreePath);
|
|
72
|
+
if (worktreeBranch !== input.worktree.branch) {
|
|
73
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", `Cannot close worktree because it is on branch ${JSON.stringify(worktreeBranch)} instead of its managed branch ${JSON.stringify(input.worktree.branch)}.`);
|
|
74
|
+
}
|
|
75
|
+
let committed = false;
|
|
76
|
+
if ((await git(["status", "--porcelain=v1"], worktreePath)).trim().length > 0) {
|
|
77
|
+
await git(["add", "-A"], worktreePath);
|
|
78
|
+
try {
|
|
79
|
+
await git(["commit", "-m", input.commitMessage], worktreePath);
|
|
80
|
+
committed = true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", `Git failed to commit the managed worktree before closing it. ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if ((await git(["status", "--porcelain=v1"], worktreePath)).trim().length > 0) {
|
|
87
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", "Cannot close worktree because it still has uncommitted changes after the close commit. A Git hook may have modified files; inspect and verify the worktree, then retry.");
|
|
88
|
+
}
|
|
89
|
+
const sourceBranchBeforeMerge = await currentBranch(sourceRoot);
|
|
90
|
+
if (sourceBranchBeforeMerge !== input.worktree.targetBranch) {
|
|
91
|
+
throw new GitWorktreeError("GIT_WORKTREE_SOURCE_BRANCH_CHANGED", `Cannot close worktree because the source checkout changed to branch ${JSON.stringify(sourceBranchBeforeMerge)} while the worktree was being finalized. The source checkout was not merged.`);
|
|
92
|
+
}
|
|
93
|
+
if ((await git(["status", "--porcelain=v1"], sourceRoot)).trim().length > 0) {
|
|
94
|
+
throw new GitWorktreeError("GIT_WORKTREE_SOURCE_DIRTY", "Cannot close worktree because the source checkout changed while the worktree was being finalized. The source checkout was not merged; commit or stash those changes and retry.");
|
|
95
|
+
}
|
|
96
|
+
const sourceHead = (await git(["rev-parse", "HEAD"], sourceRoot)).trim();
|
|
97
|
+
const commitSha = (await git(["rev-parse", "HEAD"], worktreePath)).trim();
|
|
98
|
+
if (!(await isAncestor(sourceHead, commitSha, sourceRoot))) {
|
|
99
|
+
throw new GitWorktreeError("GIT_WORKTREE_DIVERGED", `Cannot close worktree because ${input.worktree.targetBranch} advanced independently of ${input.worktree.branch}. Rebase the worktree branch onto ${input.worktree.targetBranch} inside the worktree, resolve and verify there, then retry. The source checkout was not modified.`);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await git(["merge", "--ff-only", input.worktree.branch], sourceRoot);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new GitWorktreeError("GIT_WORKTREE_DIVERGED", `Git could not fast-forward ${input.worktree.targetBranch} to ${input.worktree.branch}. The source checkout was not put into a merge-conflict state. ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
const mergedSha = (await git(["rev-parse", "HEAD"], sourceRoot)).trim();
|
|
108
|
+
try {
|
|
109
|
+
await git(["worktree", "remove", worktreePath], sourceRoot);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new GitWorktreeError("GIT_WORKTREE_CLOSE_FAILED", `Changes were merged into ${input.worktree.targetBranch}, but ForgeRelay could not remove the managed worktree. The branch and workspace are preserved so cleanup can be retried. ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
}
|
|
114
|
+
let cleanupWarning;
|
|
115
|
+
try {
|
|
116
|
+
await git(["branch", "-d", input.worktree.branch], sourceRoot);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
cleanupWarning = `The worktree was removed and its changes were merged, but Git could not delete branch ${input.worktree.branch}. Delete that already-merged branch manually if desired. ${error instanceof Error ? error.message : String(error)}`;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
sourceRoot,
|
|
123
|
+
path: worktreePath,
|
|
124
|
+
branch: input.worktree.branch,
|
|
125
|
+
targetBranch: input.worktree.targetBranch,
|
|
126
|
+
commitSha,
|
|
127
|
+
mergedSha,
|
|
128
|
+
committed,
|
|
129
|
+
cleanupWarning,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export async function resolveManagedWorktreeBase(input) {
|
|
133
|
+
const sourcePath = assertAllowedPath(input.sourcePath, input.config.allowedRoots);
|
|
134
|
+
const sourceRoot = await resolveGitRoot(sourcePath, input.config.allowedRoots);
|
|
135
|
+
const resolved = await resolveWorktreeBase(sourceRoot, input.baseRef);
|
|
136
|
+
return { sourceRoot, ...resolved };
|
|
137
|
+
}
|
|
138
|
+
async function resolveGitRoot(path, allowedRoots) {
|
|
139
|
+
try {
|
|
140
|
+
const output = await git(["rev-parse", "--show-toplevel"], path);
|
|
141
|
+
return await assertGitRootAllowed(output.trim(), allowedRoots);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (isGitUnavailable(error)) {
|
|
145
|
+
throw new GitWorktreeError("GIT_NOT_AVAILABLE", "Cannot open workspace in worktree mode because Git is not available on this machine.");
|
|
146
|
+
}
|
|
147
|
+
throw new GitWorktreeError("GIT_REPOSITORY_NOT_FOUND", `Cannot open workspace in worktree mode because this path is not inside a Git repository: ${path}. Use mode=\"checkout\" to work directly in this directory, or initialize Git and create an initial commit first.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function assertGitRootAllowed(gitRoot, allowedRoots) {
|
|
151
|
+
try {
|
|
152
|
+
return assertAllowedPath(gitRoot, allowedRoots);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
const canonicalGitRoot = await realpath(gitRoot);
|
|
156
|
+
for (const allowedRoot of allowedRoots) {
|
|
157
|
+
const canonicalAllowedRoot = await realpath(allowedRoot).catch(() => undefined);
|
|
158
|
+
if (!canonicalAllowedRoot || !isPathInsideRoot(canonicalGitRoot, canonicalAllowedRoot)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const logicalGitRoot = resolve(allowedRoot, relative(canonicalAllowedRoot, canonicalGitRoot));
|
|
162
|
+
return assertAllowedPath(logicalGitRoot, allowedRoots);
|
|
163
|
+
}
|
|
164
|
+
return assertAllowedPath(canonicalGitRoot, allowedRoots);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function resolveWorktreeBase(sourceRoot, requestedBaseRef) {
|
|
168
|
+
const targetBranch = requestedBaseRef && requestedBaseRef !== "HEAD"
|
|
169
|
+
? normalizeLocalBranchName(requestedBaseRef)
|
|
170
|
+
: await currentBranch(sourceRoot);
|
|
171
|
+
if (!targetBranch) {
|
|
172
|
+
throw new GitWorktreeError("GIT_WORKTREE_SOURCE_DETACHED", "Cannot create a managed worktree from a detached source checkout. Switch the source checkout to the branch that should receive the finished work first.");
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const baseSha = (await git(["rev-parse", "--verify", `refs/heads/${targetBranch}^{commit}`], sourceRoot)).trim();
|
|
176
|
+
return {
|
|
177
|
+
baseRef: requestedBaseRef ?? "HEAD",
|
|
178
|
+
baseSha,
|
|
179
|
+
targetBranch,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
if (!requestedBaseRef || requestedBaseRef === "HEAD") {
|
|
184
|
+
throw new GitWorktreeError("GIT_REPOSITORY_HAS_NO_COMMITS", "Cannot open workspace in worktree mode because the current branch has no commits yet. Create an initial commit first, or use checkout mode.");
|
|
185
|
+
}
|
|
186
|
+
throw new GitWorktreeError("GIT_INVALID_BASE_REF", `Cannot create a managed worktree because baseRef ${JSON.stringify(requestedBaseRef)} is not a local branch. Managed worktrees must start from the local branch they will eventually merge back into.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function currentBranch(cwd) {
|
|
190
|
+
try {
|
|
191
|
+
return (await git(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd)).trim();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function normalizeLocalBranchName(value) {
|
|
198
|
+
return value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
|
|
199
|
+
}
|
|
200
|
+
async function isAncestor(ancestor, descendant, cwd) {
|
|
201
|
+
try {
|
|
202
|
+
await git(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function managedWorktreePath(input) {
|
|
210
|
+
const repoName = sanitizePathSegment(basename(input.repoRoot)) || "repo";
|
|
211
|
+
return join(input.worktreeRoot, `${repoName}-${input.worktreeId}`);
|
|
212
|
+
}
|
|
213
|
+
function managedWorktreeBranch(input) {
|
|
214
|
+
const repoName = sanitizeGitBranchSegment(basename(input.repoRoot)) || "repo";
|
|
215
|
+
return `forgerelay/${repoName}-${input.worktreeId}`;
|
|
216
|
+
}
|
|
217
|
+
function sanitizeGitBranchSegment(value) {
|
|
218
|
+
return value
|
|
219
|
+
.replace(/[^a-zA-Z0-9_-]+/g, "-")
|
|
220
|
+
.replace(/^[-_]+|[-_]+$/g, "")
|
|
221
|
+
.slice(0, 80);
|
|
222
|
+
}
|
|
223
|
+
function sanitizePathSegment(value) {
|
|
224
|
+
return value
|
|
225
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
226
|
+
.replace(/^-+|-+$/g, "")
|
|
227
|
+
.slice(0, 80);
|
|
228
|
+
}
|
|
229
|
+
async function git(args, cwd) {
|
|
230
|
+
try {
|
|
231
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
232
|
+
cwd,
|
|
233
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
234
|
+
});
|
|
235
|
+
return stdout;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (isGitUnavailable(error))
|
|
239
|
+
throw error;
|
|
240
|
+
const stderr = typeof error === "object" && error && "stderr" in error
|
|
241
|
+
? String(error.stderr ?? "").trim()
|
|
242
|
+
: "";
|
|
243
|
+
const stdout = typeof error === "object" && error && "stdout" in error
|
|
244
|
+
? String(error.stdout ?? "").trim()
|
|
245
|
+
: "";
|
|
246
|
+
const details = stderr || stdout || (error instanceof Error ? error.message : String(error));
|
|
247
|
+
throw new Error(details);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function isGitUnavailable(error) {
|
|
251
|
+
return Boolean(typeof error === "object" &&
|
|
252
|
+
error &&
|
|
253
|
+
"code" in error &&
|
|
254
|
+
error.code === "ENOENT");
|
|
255
|
+
}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export async function git(cwd, args, options = {}) {
|
|
6
|
+
const { stdout, stderr } = await execFileAsync("git", args, {
|
|
7
|
+
cwd,
|
|
8
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
9
|
+
maxBuffer: options.maxBuffer ?? 10 * 1024 * 1024,
|
|
10
|
+
});
|
|
11
|
+
return { stdout, stderr };
|
|
12
|
+
}
|
|
13
|
+
export async function getGitEligibility(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
reason: "not_git",
|
|
21
|
+
message: "workspace is not inside a git repository",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const gitRoot = (await git(cwd, ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
25
|
+
try {
|
|
26
|
+
await git(gitRoot, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
gitRoot,
|
|
32
|
+
reason: "no_head",
|
|
33
|
+
message: "repository has no HEAD commit",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, gitRoot };
|
|
37
|
+
}
|
|
38
|
+
export function safeWorkspaceRefSegment(workspaceId) {
|
|
39
|
+
const safe = workspaceId.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
40
|
+
return safe.length > 0 ? safe : createHash("sha256").update(workspaceId).digest("hex").slice(0, 16);
|
|
41
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { basename, isAbsolute } from "node:path";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { ArtifactError } from "./artifact-error.js";
|
|
4
|
+
const ADAPTER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
5
|
+
const OPENAI_FILE_HOSTS = new Set([
|
|
6
|
+
"files.oaiusercontent.com",
|
|
7
|
+
]);
|
|
8
|
+
// ChatGPT-generated files are served from regional OpenAI-managed Azure storage
|
|
9
|
+
// accounts. Accept that account family only, never arbitrary Azure Blob hosts.
|
|
10
|
+
const OPENAI_REGIONAL_BLOB_HOST_PATTERN = /^oaisdmntpr[a-z0-9]+\.blob\.core\.windows\.net$/u;
|
|
11
|
+
const OPENAI_FILENAME_SAFE_FILE_ID_PATTERN = /^file[-_][A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u;
|
|
12
|
+
const OPENAI_FILE_ID_MAX_LENGTH = 512;
|
|
13
|
+
const OPENAI_FILE_ID_CONTROL_PATTERN = /[\u0000-\u001F\u007F]/u;
|
|
14
|
+
const OPENAI_FILE_KEYS = new Set([
|
|
15
|
+
"download_url",
|
|
16
|
+
"file_id",
|
|
17
|
+
"mime_type",
|
|
18
|
+
"file_name",
|
|
19
|
+
"name",
|
|
20
|
+
"size",
|
|
21
|
+
]);
|
|
22
|
+
const OPENAI_FILE_REDIRECT_LIMIT = 3;
|
|
23
|
+
const OPENAI_FILE_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
24
|
+
export class IncomingArtifactAdapterRegistry {
|
|
25
|
+
adapters;
|
|
26
|
+
constructor(adapters = []) {
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
for (const adapter of adapters) {
|
|
29
|
+
if (!ADAPTER_ID_PATTERN.test(adapter.id)) {
|
|
30
|
+
throw new ArtifactError("invalid_incoming_adapter", "Incoming artifact adapter IDs must be short lowercase identifiers.");
|
|
31
|
+
}
|
|
32
|
+
if (ids.has(adapter.id)) {
|
|
33
|
+
throw new ArtifactError("duplicate_incoming_adapter", `Incoming artifact adapter '${adapter.id}' is registered more than once.`);
|
|
34
|
+
}
|
|
35
|
+
ids.add(adapter.id);
|
|
36
|
+
}
|
|
37
|
+
this.adapters = [...adapters];
|
|
38
|
+
}
|
|
39
|
+
async open(value) {
|
|
40
|
+
const matching = [];
|
|
41
|
+
for (const adapter of this.adapters) {
|
|
42
|
+
let handles = false;
|
|
43
|
+
try {
|
|
44
|
+
handles = adapter.canHandle(value);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new ArtifactError("incoming_artifact_adapter_failed", `Incoming artifact adapter '${adapter.id}' failed during recognition.`);
|
|
48
|
+
}
|
|
49
|
+
if (handles)
|
|
50
|
+
matching.push(adapter);
|
|
51
|
+
}
|
|
52
|
+
if (matching.length === 0) {
|
|
53
|
+
throw new ArtifactError("unsupported_incoming_artifact", "No trusted incoming artifact adapter recognized this file reference.");
|
|
54
|
+
}
|
|
55
|
+
if (matching.length > 1) {
|
|
56
|
+
throw new ArtifactError("ambiguous_incoming_artifact", "More than one trusted incoming artifact adapter recognized this file reference.");
|
|
57
|
+
}
|
|
58
|
+
const adapter = matching[0];
|
|
59
|
+
let source;
|
|
60
|
+
try {
|
|
61
|
+
source = await adapter.open(value);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof ArtifactError)
|
|
65
|
+
throw error;
|
|
66
|
+
throw new ArtifactError("incoming_artifact_open_failed", `Incoming artifact adapter '${adapter.id}' could not open the file reference.`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
validateIncomingArtifactSource(source);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
source?.stream?.destroy?.();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return { ...source, adapterId: adapter.id };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function createOpenAIIncomingArtifactAdapter(options = {}) {
|
|
79
|
+
const fetchFile = options.fetch ?? globalThis.fetch;
|
|
80
|
+
const timeoutMs = options.timeoutMs ?? OPENAI_FILE_DOWNLOAD_TIMEOUT_MS;
|
|
81
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
|
|
82
|
+
throw new ArtifactError("invalid_openai_file_adapter", "OpenAI file download timeout must be a positive integer.");
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: "openai-file",
|
|
86
|
+
canHandle: isOpenAIFileReferenceCandidate,
|
|
87
|
+
async open(value) {
|
|
88
|
+
const reference = normalizeOpenAIFileReference(value);
|
|
89
|
+
let downloadUrl = validateOpenAIFileUrl(reference.download_url);
|
|
90
|
+
let response;
|
|
91
|
+
for (let redirect = 0; redirect <= OPENAI_FILE_REDIRECT_LIMIT; redirect += 1) {
|
|
92
|
+
try {
|
|
93
|
+
response = await fetchFile(downloadUrl, {
|
|
94
|
+
redirect: "manual",
|
|
95
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new ArtifactError("openai_file_download_failed", "ChatGPT file could not be downloaded.");
|
|
100
|
+
}
|
|
101
|
+
if (!isRedirectStatus(response.status))
|
|
102
|
+
break;
|
|
103
|
+
const location = response.headers.get("location");
|
|
104
|
+
await response.body?.cancel().catch(() => undefined);
|
|
105
|
+
if (!location || redirect === OPENAI_FILE_REDIRECT_LIMIT) {
|
|
106
|
+
throw new ArtifactError("openai_file_download_failed", "ChatGPT file download returned an invalid redirect.");
|
|
107
|
+
}
|
|
108
|
+
downloadUrl = validateOpenAIFileUrl(new URL(location, downloadUrl).toString());
|
|
109
|
+
}
|
|
110
|
+
if (!response?.ok || !response.body) {
|
|
111
|
+
await response?.body?.cancel().catch(() => undefined);
|
|
112
|
+
throw new ArtifactError("openai_file_download_failed", "ChatGPT file download did not return file content.");
|
|
113
|
+
}
|
|
114
|
+
const responseSize = responseContentLength(response);
|
|
115
|
+
if (reference.size !== undefined
|
|
116
|
+
&& responseSize !== undefined
|
|
117
|
+
&& reference.size !== responseSize) {
|
|
118
|
+
await response.body.cancel().catch(() => undefined);
|
|
119
|
+
throw new ArtifactError("openai_file_size_mismatch", "ChatGPT file metadata did not match the downloaded content.");
|
|
120
|
+
}
|
|
121
|
+
const mimeType = reference.mime_type ?? responseMimeType(response);
|
|
122
|
+
return {
|
|
123
|
+
name: normalizeOpenAIFileName(reference.file_name, reference.file_id, mimeType),
|
|
124
|
+
mimeType,
|
|
125
|
+
size: responseSize ?? reference.size,
|
|
126
|
+
stream: Readable.fromWeb(response.body),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function describeIncomingArtifactValue(value, maxDepth = 4, maxEntries = 20) {
|
|
132
|
+
const seen = new WeakSet();
|
|
133
|
+
const describe = (current, depth) => {
|
|
134
|
+
if (current === null)
|
|
135
|
+
return { type: "null" };
|
|
136
|
+
if (current === undefined)
|
|
137
|
+
return { type: "undefined" };
|
|
138
|
+
if (typeof current === "boolean")
|
|
139
|
+
return { type: "boolean" };
|
|
140
|
+
if (typeof current === "number")
|
|
141
|
+
return { type: "number", finite: Number.isFinite(current) };
|
|
142
|
+
if (typeof current === "bigint")
|
|
143
|
+
return { type: "bigint" };
|
|
144
|
+
if (typeof current === "function")
|
|
145
|
+
return { type: "function" };
|
|
146
|
+
if (typeof current === "symbol")
|
|
147
|
+
return { type: "symbol" };
|
|
148
|
+
if (typeof current === "string") {
|
|
149
|
+
return {
|
|
150
|
+
type: "string",
|
|
151
|
+
kind: classifyValueString(current),
|
|
152
|
+
length: current.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (seen.has(current))
|
|
156
|
+
return { type: "cycle" };
|
|
157
|
+
seen.add(current);
|
|
158
|
+
if (Array.isArray(current)) {
|
|
159
|
+
if (depth >= maxDepth) {
|
|
160
|
+
return { type: "array", length: current.length, items: [], truncated: current.length > 0 };
|
|
161
|
+
}
|
|
162
|
+
const items = current.slice(0, maxEntries).map((item) => describe(item, depth + 1));
|
|
163
|
+
return {
|
|
164
|
+
type: "array",
|
|
165
|
+
length: current.length,
|
|
166
|
+
items,
|
|
167
|
+
truncated: current.length > items.length,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const keys = Object.keys(current).sort();
|
|
171
|
+
if (depth >= maxDepth) {
|
|
172
|
+
return {
|
|
173
|
+
type: "object",
|
|
174
|
+
constructor: safeConstructorName(current),
|
|
175
|
+
entries: {},
|
|
176
|
+
truncated: keys.length > 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const entries = {};
|
|
180
|
+
for (const [index, key] of keys.slice(0, maxEntries).entries()) {
|
|
181
|
+
let entryValue;
|
|
182
|
+
try {
|
|
183
|
+
entryValue = current[key];
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
entryValue = undefined;
|
|
187
|
+
}
|
|
188
|
+
entries[safeValueEntryKey(key, index)] = describe(entryValue, depth + 1);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
type: "object",
|
|
192
|
+
constructor: safeConstructorName(current),
|
|
193
|
+
entries,
|
|
194
|
+
truncated: keys.length > Object.keys(entries).length,
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
return describe(value, 0);
|
|
198
|
+
}
|
|
199
|
+
function validateIncomingArtifactSource(source) {
|
|
200
|
+
if (!source || typeof source !== "object") {
|
|
201
|
+
throw new ArtifactError("invalid_incoming_artifact_source", "Incoming artifact adapter returned an invalid source.");
|
|
202
|
+
}
|
|
203
|
+
if (typeof source.name !== "string" || source.name.length === 0) {
|
|
204
|
+
throw new ArtifactError("invalid_incoming_artifact_source", "Incoming artifact adapter must provide a filename.");
|
|
205
|
+
}
|
|
206
|
+
if (source.mimeType !== undefined && typeof source.mimeType !== "string") {
|
|
207
|
+
throw new ArtifactError("invalid_incoming_artifact_source", "Incoming artifact adapter returned an invalid MIME hint.");
|
|
208
|
+
}
|
|
209
|
+
if (source.size !== undefined
|
|
210
|
+
&& (!Number.isSafeInteger(source.size) || source.size < 0)) {
|
|
211
|
+
throw new ArtifactError("invalid_incoming_artifact_source", "Incoming artifact adapter returned an invalid byte size.");
|
|
212
|
+
}
|
|
213
|
+
const stream = source.stream;
|
|
214
|
+
if (!stream || typeof stream[Symbol.asyncIterator] !== "function") {
|
|
215
|
+
throw new ArtifactError("invalid_incoming_artifact_source", "Incoming artifact adapter must provide an async-readable stream.");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function isOpenAIFileReferenceCandidate(value) {
|
|
219
|
+
if (!isRecord(value))
|
|
220
|
+
return false;
|
|
221
|
+
const keys = Object.keys(value);
|
|
222
|
+
return keys.length >= 2
|
|
223
|
+
&& keys.every((key) => OPENAI_FILE_KEYS.has(key))
|
|
224
|
+
&& Object.hasOwn(value, "download_url")
|
|
225
|
+
&& Object.hasOwn(value, "file_id");
|
|
226
|
+
}
|
|
227
|
+
function normalizeOpenAIFileReference(value) {
|
|
228
|
+
if (!isOpenAIFileReferenceCandidate(value)) {
|
|
229
|
+
throw new ArtifactError("invalid_openai_file_reference", "ChatGPT file reference is malformed.");
|
|
230
|
+
}
|
|
231
|
+
const downloadUrl = value.download_url;
|
|
232
|
+
const fileId = value.file_id;
|
|
233
|
+
if (typeof downloadUrl !== "string"
|
|
234
|
+
|| typeof fileId !== "string"
|
|
235
|
+
|| !isValidOpenAIFileId(fileId)) {
|
|
236
|
+
throw new ArtifactError("invalid_openai_file_reference", "ChatGPT file reference is malformed.");
|
|
237
|
+
}
|
|
238
|
+
const mimeType = nullableString(value.mime_type);
|
|
239
|
+
const fileName = nullableString(value.file_name);
|
|
240
|
+
const nameAlias = nullableString(value.name);
|
|
241
|
+
if (mimeType === null || fileName === null || nameAlias === null) {
|
|
242
|
+
throw new ArtifactError("invalid_openai_file_reference", "ChatGPT file reference is malformed.");
|
|
243
|
+
}
|
|
244
|
+
const normalizedFileName = normalizeSuppliedOpenAIFileName(fileName);
|
|
245
|
+
const normalizedNameAlias = normalizeSuppliedOpenAIFileName(nameAlias);
|
|
246
|
+
if (normalizedFileName
|
|
247
|
+
&& normalizedNameAlias
|
|
248
|
+
&& normalizedFileName !== normalizedNameAlias) {
|
|
249
|
+
throw new ArtifactError("ambiguous_openai_file_name", "ChatGPT file reference contained conflicting filenames.");
|
|
250
|
+
}
|
|
251
|
+
let size;
|
|
252
|
+
const rawSize = value.size;
|
|
253
|
+
if (rawSize !== undefined && rawSize !== null) {
|
|
254
|
+
if (typeof rawSize !== "number" || !Number.isSafeInteger(rawSize) || rawSize < 0) {
|
|
255
|
+
throw new ArtifactError("invalid_openai_file_reference", "ChatGPT file reference is malformed.");
|
|
256
|
+
}
|
|
257
|
+
size = rawSize;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
download_url: downloadUrl,
|
|
261
|
+
file_id: fileId,
|
|
262
|
+
mime_type: mimeType,
|
|
263
|
+
file_name: normalizedFileName ?? normalizedNameAlias,
|
|
264
|
+
size,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function nullableString(value) {
|
|
268
|
+
if (value === undefined || value === null)
|
|
269
|
+
return undefined;
|
|
270
|
+
return typeof value === "string" ? value : null;
|
|
271
|
+
}
|
|
272
|
+
function normalizeOpenAIFileName(suppliedName, fileId, mimeType) {
|
|
273
|
+
if (suppliedName)
|
|
274
|
+
return suppliedName;
|
|
275
|
+
const safeBaseName = OPENAI_FILENAME_SAFE_FILE_ID_PATTERN.test(fileId)
|
|
276
|
+
? fileId
|
|
277
|
+
: "chatgpt-file";
|
|
278
|
+
return `${safeBaseName}${extensionForMimeType(mimeType) ?? ".bin"}`;
|
|
279
|
+
}
|
|
280
|
+
function isValidOpenAIFileId(value) {
|
|
281
|
+
return value.length > 0
|
|
282
|
+
&& value.length <= OPENAI_FILE_ID_MAX_LENGTH
|
|
283
|
+
&& !OPENAI_FILE_ID_CONTROL_PATTERN.test(value);
|
|
284
|
+
}
|
|
285
|
+
function normalizeSuppliedOpenAIFileName(value) {
|
|
286
|
+
if (!value)
|
|
287
|
+
return undefined;
|
|
288
|
+
const normalized = value.replaceAll("\\", "/");
|
|
289
|
+
const candidate = basename(normalized).trim();
|
|
290
|
+
if (!candidate || candidate === "." || candidate === ".." || candidate.startsWith(".")) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
return candidate;
|
|
294
|
+
}
|
|
295
|
+
function extensionForMimeType(mimeType) {
|
|
296
|
+
switch (mimeType?.toLowerCase()) {
|
|
297
|
+
case "image/png": return ".png";
|
|
298
|
+
case "image/jpeg": return ".jpg";
|
|
299
|
+
case "image/webp": return ".webp";
|
|
300
|
+
case "image/gif": return ".gif";
|
|
301
|
+
case "application/pdf": return ".pdf";
|
|
302
|
+
case "text/plain": return ".txt";
|
|
303
|
+
case "application/zip": return ".zip";
|
|
304
|
+
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": return ".docx";
|
|
305
|
+
default: return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function validateOpenAIFileUrl(value) {
|
|
309
|
+
let url;
|
|
310
|
+
try {
|
|
311
|
+
url = new URL(value);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new ArtifactError("unsafe_openai_file_reference", "ChatGPT file download URL is invalid.");
|
|
315
|
+
}
|
|
316
|
+
if (url.protocol !== "https:"
|
|
317
|
+
|| !isTrustedOpenAIFileHost(url.hostname)
|
|
318
|
+
|| (url.port !== "" && url.port !== "443")
|
|
319
|
+
|| url.username !== ""
|
|
320
|
+
|| url.password !== ""
|
|
321
|
+
|| url.hash !== "") {
|
|
322
|
+
throw new ArtifactError("unsafe_openai_file_reference", "ChatGPT file download URL is outside the trusted file host.");
|
|
323
|
+
}
|
|
324
|
+
return url.toString();
|
|
325
|
+
}
|
|
326
|
+
function isTrustedOpenAIFileHost(hostname) {
|
|
327
|
+
return OPENAI_FILE_HOSTS.has(hostname) || OPENAI_REGIONAL_BLOB_HOST_PATTERN.test(hostname);
|
|
328
|
+
}
|
|
329
|
+
function isRedirectStatus(status) {
|
|
330
|
+
return status === 301
|
|
331
|
+
|| status === 302
|
|
332
|
+
|| status === 303
|
|
333
|
+
|| status === 307
|
|
334
|
+
|| status === 308;
|
|
335
|
+
}
|
|
336
|
+
function responseMimeType(response) {
|
|
337
|
+
const value = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
|
|
338
|
+
return value || undefined;
|
|
339
|
+
}
|
|
340
|
+
function responseContentLength(response) {
|
|
341
|
+
const value = response.headers.get("content-length");
|
|
342
|
+
if (!value || !/^\d+$/u.test(value))
|
|
343
|
+
return undefined;
|
|
344
|
+
const size = Number(value);
|
|
345
|
+
return Number.isSafeInteger(size) ? size : undefined;
|
|
346
|
+
}
|
|
347
|
+
function isRecord(value) {
|
|
348
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
349
|
+
}
|
|
350
|
+
function classifyValueString(value) {
|
|
351
|
+
if (value.startsWith("data:"))
|
|
352
|
+
return "data-url";
|
|
353
|
+
if (isAbsolute(value))
|
|
354
|
+
return "absolute-path";
|
|
355
|
+
try {
|
|
356
|
+
const parsed = new URL(value);
|
|
357
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:")
|
|
358
|
+
return "url";
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
// Non-URL strings are summarized only by type and length.
|
|
362
|
+
}
|
|
363
|
+
return "text";
|
|
364
|
+
}
|
|
365
|
+
function safeValueEntryKey(value, index) {
|
|
366
|
+
return /^[A-Za-z_][A-Za-z0-9_.-]{0,79}$/u.test(value)
|
|
367
|
+
? value
|
|
368
|
+
: `<redacted-key-${index + 1}>`;
|
|
369
|
+
}
|
|
370
|
+
function safeConstructorName(value) {
|
|
371
|
+
try {
|
|
372
|
+
const name = value.constructor?.name;
|
|
373
|
+
return typeof name === "string" && name.length <= 80 ? name : undefined;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|