@armanckeser/downstream 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/SKILL.md +150 -0
- package/bin/downstream.mjs +19 -0
- package/dist/cli/downstream.js +543 -0
- package/dist/domain/agent.js +3 -0
- package/dist/domain/approval.js +190 -0
- package/dist/domain/delivery.js +21 -0
- package/dist/domain/home.js +1 -0
- package/dist/domain/inbox.js +245 -0
- package/dist/domain/model.js +180 -0
- package/dist/domain/next-move.js +34 -0
- package/dist/domain/pr-detail.js +45 -0
- package/dist/server/actions.js +564 -0
- package/dist/server/agent-host.js +230 -0
- package/dist/server/analyze/git.js +161 -0
- package/dist/server/analyze/graph.js +403 -0
- package/dist/server/analyze/py-graph.js +141 -0
- package/dist/server/analyze/py.js +346 -0
- package/dist/server/analyze/ts-decls.js +299 -0
- package/dist/server/analyze/ts-project.js +212 -0
- package/dist/server/gh.js +51 -0
- package/dist/server/github.js +264 -0
- package/dist/server/home-store.js +127 -0
- package/dist/server/home.js +325 -0
- package/dist/server/install.js +12 -0
- package/dist/server/main.js +228 -0
- package/dist/server/prompts.js +32 -0
- package/dist/server/publish.js +136 -0
- package/dist/server/repos.js +164 -0
- package/dist/server/stale.js +50 -0
- package/dist/server/store.js +269 -0
- package/dist/server/workspace.js +251 -0
- package/package.json +63 -4
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-iITFzleb.js +1 -0
- package/web/dist/assets/angular-ts-qNGaWUZr.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-DAcxbtpZ.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-CxPQu5s8.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-DJrnOqL4.js +1 -0
- package/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/web/dist/assets/c-DJLzn8qa.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-BHYX99F3.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-C_LDam8n.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-CcuifkxZ.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/cpp-E7p80IG9.js +1 -0
- package/web/dist/assets/crystal-iYHODIir.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-Bk6njqO4.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-Ct7_SkpN.js +1 -0
- package/web/dist/assets/elixir-YTzSQL2h.js +1 -0
- package/web/dist/assets/elm-bNkwlHr8.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/erb-uFBspDdG.js +1 -0
- package/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/web/dist/assets/git-rebase-CSzaMu4k.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-WrpQIag3.js +1 -0
- package/web/dist/assets/glimmer-ts-BkR7s81M.js +1 -0
- package/web/dist/assets/glsl-r6ahr8UN.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-vcFzEvUM.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-DV1TStQR.js +1 -0
- package/web/dist/assets/haml-BzEpEFnM.js +1 -0
- package/web/dist/assets/handlebars-Bw2ai2tn.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CVWJgEI4.js +1 -0
- package/web/dist/assets/html-derivative-kTFEcoss.js +1 -0
- package/web/dist/assets/http-DXC6PGgt.js +1 -0
- package/web/dist/assets/hurl-Bpd8P_ef.js +1 -0
- package/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DpYMsPxu.js +1673 -0
- package/web/dist/assets/index-Zh-Bd2u6.css +2 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-Bsmh9nXE.js +1 -0
- package/web/dist/assets/javascript-CeyaFN_P.js +1 -0
- package/web/dist/assets/jinja-BuqTDofy.js +1 -0
- package/web/dist/assets/jison-BYfmmaBZ.js +1 -0
- package/web/dist/assets/json-Bhl6cDM_.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-BSUHjwDX.js +1 -0
- package/web/dist/assets/julia-CWrNF0wf.js +1 -0
- package/web/dist/assets/just-wLw7gWsD.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-DyWaeH0P.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-DeCc5Rg2.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-Ba-NltBp.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-DGb2HMVj.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-CL7bhRQ-.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-CSgcYZcx.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-CbyuJibN.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-C7LemgDR.js +1 -0
- package/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-wrbLazmP.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-DarHXyEu.js +1 -0
- package/web/dist/assets/php--54M0-pj.js +1 -0
- package/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-CFrC1rDC.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-Go4B46qX.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-Bpu17F3f.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-CeZTEIIg.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-D4K6CiTG.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-Dy2Bl1Fg.js +1 -0
- package/web/dist/assets/ruby-CNqr2Wuz.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-B6MsvXFb.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CHuFWGi8.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/web/dist/assets/shellscript-EEdMjEbs.js +1 -0
- package/web/dist/assets/shellsession-U_I5Q2cJ.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-BnCBZkG3.js +1 -0
- package/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-5dStcFSw.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-B5iSQdtS.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-D4K5msa4.js +1 -0
- package/web/dist/assets/svelte-Dn-_7BgE.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-BTe8yLW8.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-BBG77enc.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-BzJXXnMD.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-Cj2inrAP.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-BH3HU2q4.js +1 -0
- package/web/dist/assets/typescript-DpDozqSz.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-GHzvrUad.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CqkncxvB.js +1 -0
- package/web/dist/assets/vue-html-DrD64fqb.js +1 -0
- package/web/dist/assets/vue-vine-BJy37wv2.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-CtXDvb3g.js +1 -0
- package/web/dist/assets/xsl-9J2XPEqv.js +1 -0
- package/web/dist/assets/yaml-CKsrpaXo.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// The home page's server side: the inbox for the account you're looking
|
|
2
|
+
// through (cached, refreshed in the background), sorted into the matrix with
|
|
3
|
+
// your choices applied, and starting the agent on a pull request.
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { streamSSE } from "hono/streaming";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { arrange, MOVES, Quadrant, toItems, withTouches } from "../domain/inbox.js";
|
|
8
|
+
import { prKey } from "../domain/agent.js";
|
|
9
|
+
import { nextMove } from "../domain/next-move.js";
|
|
10
|
+
import { accounts } from "./gh.js";
|
|
11
|
+
import { fetchAvatar, fetchDetail, fetchPrs, fetchTouches } from "./github.js";
|
|
12
|
+
import { promptFor, stalePrompt } from "./prompts.js";
|
|
13
|
+
import { decide, GitHubRefused, NotOffered, StaleDecision } from "./stale.js";
|
|
14
|
+
import { ActionError } from "./actions.js";
|
|
15
|
+
const STALE_AFTER_MS = 60_000;
|
|
16
|
+
/** One account's PRs, fetched at most once at a time, kept until the next fetch lands. */
|
|
17
|
+
class InboxCache {
|
|
18
|
+
snapshots = new Map();
|
|
19
|
+
inflight = new Map();
|
|
20
|
+
errors = new Map();
|
|
21
|
+
onChange;
|
|
22
|
+
constructor(onChange) {
|
|
23
|
+
this.onChange = onChange;
|
|
24
|
+
}
|
|
25
|
+
get(account) {
|
|
26
|
+
return this.snapshots.get(account) ?? null;
|
|
27
|
+
}
|
|
28
|
+
error(account) {
|
|
29
|
+
return this.errors.get(account) ?? null;
|
|
30
|
+
}
|
|
31
|
+
refreshing(account) {
|
|
32
|
+
return this.inflight.has(account);
|
|
33
|
+
}
|
|
34
|
+
/** Fetch if never fetched or stale (or `force`); returns once the fetch is done. */
|
|
35
|
+
refresh(account, force = false) {
|
|
36
|
+
const snap = this.get(account);
|
|
37
|
+
const fresh = snap && Date.now() - Date.parse(snap.at) < STALE_AFTER_MS;
|
|
38
|
+
if (fresh && !force)
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
const running = this.inflight.get(account);
|
|
41
|
+
if (running)
|
|
42
|
+
return running;
|
|
43
|
+
// Without the activity feed the inbox still works, just without knowing your focus.
|
|
44
|
+
const p = Promise.all([fetchPrs(account), fetchTouches(account).catch(() => [])])
|
|
45
|
+
.then(([prs, touches]) => {
|
|
46
|
+
this.snapshots.set(account, { prs: withTouches(prs, touches), touches, at: new Date().toISOString() });
|
|
47
|
+
this.errors.delete(account);
|
|
48
|
+
})
|
|
49
|
+
.catch((e) => {
|
|
50
|
+
this.errors.set(account, e.message.split("\n").slice(0, 2).join(" "));
|
|
51
|
+
})
|
|
52
|
+
.finally(() => {
|
|
53
|
+
this.inflight.delete(account);
|
|
54
|
+
this.onChange();
|
|
55
|
+
});
|
|
56
|
+
this.inflight.set(account, p);
|
|
57
|
+
this.onChange();
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function homeRoutes(deps) {
|
|
62
|
+
const { home, repos, agents } = deps;
|
|
63
|
+
const app = new Hono();
|
|
64
|
+
const cache = new InboxCache(() => home.changed());
|
|
65
|
+
let known = null;
|
|
66
|
+
const accountList = () => (known ??= accounts());
|
|
67
|
+
const avatars = new Map();
|
|
68
|
+
const avatarOf = (login) => {
|
|
69
|
+
if (!avatars.has(login)) {
|
|
70
|
+
avatars.set(login, null);
|
|
71
|
+
void fetchAvatar(login).then((url) => {
|
|
72
|
+
avatars.set(login, url);
|
|
73
|
+
if (url)
|
|
74
|
+
home.changed();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return avatars.get(login) ?? null;
|
|
78
|
+
};
|
|
79
|
+
const currentAccount = () => {
|
|
80
|
+
const chosen = home.account();
|
|
81
|
+
const list = accountList();
|
|
82
|
+
return chosen && list.includes(chosen) ? chosen : (list[0] ?? null);
|
|
83
|
+
};
|
|
84
|
+
const items = (account) => toItems(cache.get(account)?.prs ?? []);
|
|
85
|
+
function sessions() {
|
|
86
|
+
const out = {};
|
|
87
|
+
const at = (key) => (out[key] ??= { agent: null, review: null });
|
|
88
|
+
for (const v of agents.views()) {
|
|
89
|
+
// What the agent said, never the commands it ran: those are its business, not yours.
|
|
90
|
+
const said = [...v.activity].reverse().find((a) => a.kind === "text");
|
|
91
|
+
const line = said?.kind === "text" ? said.text.split("\n")[0].slice(0, 140) : null;
|
|
92
|
+
at(prKey(v.repo, v.pr)).agent = { status: v.status, line, approvals: v.approvals.length };
|
|
93
|
+
}
|
|
94
|
+
for (const repo of repos.withReviews()) {
|
|
95
|
+
if (!repo.nameWithOwner)
|
|
96
|
+
continue;
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const review of repo.store.listReviews()) {
|
|
99
|
+
if (review.pr === null || seen.has(review.pr))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(review.pr);
|
|
102
|
+
const state = repo.store.state(review.id);
|
|
103
|
+
if (!state)
|
|
104
|
+
continue;
|
|
105
|
+
at(prKey(repo.nameWithOwner, review.pr)).review = { path: `/r/${repo.id}?review=${review.id}`, title: review.title, move: nextMove(state, new Set(state.visited)) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function view() {
|
|
111
|
+
const account = currentAccount();
|
|
112
|
+
const snap = account ? cache.get(account) : null;
|
|
113
|
+
if (account)
|
|
114
|
+
void cache.refresh(account);
|
|
115
|
+
return {
|
|
116
|
+
accounts: accountList().map((login) => ({ login, avatar: avatarOf(login) })),
|
|
117
|
+
account,
|
|
118
|
+
inbox: snap && account ? arrange(items(account), home.choices(account), new Date(), snap.touches) : null,
|
|
119
|
+
fetchedAt: snap?.at ?? null,
|
|
120
|
+
refreshing: account ? cache.refreshing(account) : false,
|
|
121
|
+
error: account ? cache.error(account) : "gh isn't logged in to any GitHub account. Run `gh auth login`.",
|
|
122
|
+
sessions: sessions(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** A PR in the stale sweep, or a 404 the browser shows as "refresh". */
|
|
126
|
+
function staleItem(account, key) {
|
|
127
|
+
const inbox = arrange(items(account), home.choices(account), new Date(), cache.get(account)?.touches ?? []);
|
|
128
|
+
const item = inbox.stale.find((i) => i.key === key);
|
|
129
|
+
if (!item)
|
|
130
|
+
throw new ActionError("That pull request isn't stale any more. Refresh.", 404);
|
|
131
|
+
return item;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Start (or resume) the agent on an item. Returns where the browser should go.
|
|
135
|
+
* `stale` asks for a recommendation instead of the item's move.
|
|
136
|
+
*/
|
|
137
|
+
async function start(account, key, ask = "move") {
|
|
138
|
+
const item = ask === "stale" ? staleItem(account, key) : items(account).find((i) => i.key === key);
|
|
139
|
+
if (!item)
|
|
140
|
+
throw new ActionError("That pull request isn't in your inbox any more. Refresh.", 404);
|
|
141
|
+
const pr = item.lead;
|
|
142
|
+
const opens = MOVES[item.move.kind].opens;
|
|
143
|
+
if (ask === "move" && opens === "github")
|
|
144
|
+
return { url: pr.url };
|
|
145
|
+
const agentKey = prKey(pr.repo, pr.number);
|
|
146
|
+
const existing = sessions()[agentKey];
|
|
147
|
+
const where = existing?.review ? { path: existing.review.path } : { path: `/s/${encodeURIComponent(agentKey)}` };
|
|
148
|
+
if (agents.live(agentKey))
|
|
149
|
+
return where;
|
|
150
|
+
const root = repos.find(pr.repo) ?? (await repos.clone(pr.repo, account));
|
|
151
|
+
repos.register(root);
|
|
152
|
+
const prompt = ask === "stale" ? stalePrompt(item) : promptFor(item);
|
|
153
|
+
if (!prompt)
|
|
154
|
+
return { url: pr.url };
|
|
155
|
+
const previous = home.launch(pr.repo, pr.number);
|
|
156
|
+
home.putLaunch({ account, repo: pr.repo, pr: pr.number, root, sessionId: null });
|
|
157
|
+
const resumeId = previous?.root === root ? previous.sessionId : null;
|
|
158
|
+
agents.start({ key: agentKey, repo: pr.repo, pr: pr.number, root, account, prompt: resumeId ? "I'm back. Pick up where we left off; run `downstream wait` if you're mid-review." : prompt, resumeId });
|
|
159
|
+
return where;
|
|
160
|
+
}
|
|
161
|
+
// The session id appears a moment after start; keep it so a later visit can resume.
|
|
162
|
+
agents.bus.on("changed", (key) => {
|
|
163
|
+
const v = agents.view(key);
|
|
164
|
+
if (v?.sessionId) {
|
|
165
|
+
const launch = home.launch(v.repo, v.pr);
|
|
166
|
+
if (launch && launch.sessionId !== v.sessionId)
|
|
167
|
+
home.putLaunch({ ...launch, sessionId: v.sessionId });
|
|
168
|
+
}
|
|
169
|
+
home.changed();
|
|
170
|
+
});
|
|
171
|
+
// A message you write in a review reaches an agent that went idle.
|
|
172
|
+
const WAKES = new Set(["ask", "note.replied", "note.added", "review.verdict"]);
|
|
173
|
+
repos.bus.on("event", (repo, reviewId, e) => {
|
|
174
|
+
home.changed();
|
|
175
|
+
if (e.actor !== "user" || !WAKES.has(e.type) || !repo.nameWithOwner)
|
|
176
|
+
return;
|
|
177
|
+
const pr = repo.store.review(reviewId)?.pr;
|
|
178
|
+
if (pr != null)
|
|
179
|
+
agents.nudge(prKey(repo.nameWithOwner, pr));
|
|
180
|
+
});
|
|
181
|
+
app.get("/home", async (c) => {
|
|
182
|
+
const account = currentAccount();
|
|
183
|
+
if (account && !cache.get(account))
|
|
184
|
+
await cache.refresh(account);
|
|
185
|
+
return c.json(view());
|
|
186
|
+
});
|
|
187
|
+
// Details change with every push; a minute keeps j/k through the list instant without going stale.
|
|
188
|
+
const details = new Map();
|
|
189
|
+
app.get("/prs/:owner/:name/:number", async (c) => {
|
|
190
|
+
const account = currentAccount();
|
|
191
|
+
if (!account)
|
|
192
|
+
return c.json({ error: "No GitHub account." }, 400);
|
|
193
|
+
const repo = `${c.req.param("owner")}/${c.req.param("name")}`;
|
|
194
|
+
const number = Number(c.req.param("number"));
|
|
195
|
+
const key = `${account}:${repo}#${number}`;
|
|
196
|
+
let hit = details.get(key);
|
|
197
|
+
if (!hit || Date.now() - hit.at > STALE_AFTER_MS) {
|
|
198
|
+
hit = { at: Date.now(), detail: fetchDetail(account, repo, number) };
|
|
199
|
+
details.set(key, hit);
|
|
200
|
+
hit.detail.catch(() => details.delete(key));
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return c.json(await hit.detail);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
return c.json({ error: e.message.split("\n")[0] }, 502);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
app.get("/agents/:key", (c) => {
|
|
210
|
+
const v = agents.view(c.req.param("key"));
|
|
211
|
+
return v ? c.json(v) : c.json({ error: "No agent session for this pull request." }, 404);
|
|
212
|
+
});
|
|
213
|
+
const HomeActions = {
|
|
214
|
+
"home.account": z.object({ login: z.string() }),
|
|
215
|
+
"home.choose": z.object({ key: z.string(), quadrant: Quadrant.nullable() }),
|
|
216
|
+
"home.refresh": z.object({}),
|
|
217
|
+
"home.start": z.object({ key: z.string() }),
|
|
218
|
+
"stale.decide": z.object({ key: z.string(), decision: StaleDecision }),
|
|
219
|
+
"stale.ask": z.object({ key: z.string() }),
|
|
220
|
+
"agent.send": z.object({ key: z.string(), text: z.string().min(1) }),
|
|
221
|
+
"agent.decide": z.object({ key: z.string(), approvalId: z.string(), allow: z.boolean() }),
|
|
222
|
+
"agent.handoff": z.object({ key: z.string() }),
|
|
223
|
+
};
|
|
224
|
+
app.post("/home/actions/:name", async (c) => {
|
|
225
|
+
const name = c.req.param("name");
|
|
226
|
+
const schema = HomeActions[name];
|
|
227
|
+
if (!schema)
|
|
228
|
+
return c.json({ error: `Unknown action ${name}` }, 404);
|
|
229
|
+
const input = schema.parse((await c.req.json().catch(() => ({}))).input ?? {});
|
|
230
|
+
const account = currentAccount();
|
|
231
|
+
switch (name) {
|
|
232
|
+
case "home.account": {
|
|
233
|
+
const { login } = input;
|
|
234
|
+
if (!accountList().includes(login))
|
|
235
|
+
throw new ActionError(`gh isn't logged in as ${login}.`);
|
|
236
|
+
home.setAccount(login);
|
|
237
|
+
void cache.refresh(login);
|
|
238
|
+
return c.json({ ok: true });
|
|
239
|
+
}
|
|
240
|
+
case "home.choose": {
|
|
241
|
+
if (!account)
|
|
242
|
+
throw new ActionError("No GitHub account.");
|
|
243
|
+
const { key, quadrant } = input;
|
|
244
|
+
home.choose(account, key, quadrant);
|
|
245
|
+
return c.json({ ok: true });
|
|
246
|
+
}
|
|
247
|
+
case "home.refresh":
|
|
248
|
+
if (account)
|
|
249
|
+
await cache.refresh(account, true);
|
|
250
|
+
return c.json({ ok: true });
|
|
251
|
+
case "home.start": {
|
|
252
|
+
if (!account)
|
|
253
|
+
throw new ActionError("No GitHub account.");
|
|
254
|
+
return c.json(await start(account, input.key));
|
|
255
|
+
}
|
|
256
|
+
case "stale.decide": {
|
|
257
|
+
if (!account)
|
|
258
|
+
throw new ActionError("No GitHub account.");
|
|
259
|
+
const { key, decision } = input;
|
|
260
|
+
try {
|
|
261
|
+
await decide(account, staleItem(account, key), decision, () => home.choose(account, key, "hidden"));
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
if (e instanceof NotOffered)
|
|
265
|
+
throw new ActionError(e.message, 422);
|
|
266
|
+
if (e instanceof GitHubRefused)
|
|
267
|
+
throw new ActionError(`GitHub refused: ${e.message}`, 502);
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
// The hide shows at once; what GitHub now says arrives with the next fetch.
|
|
271
|
+
void cache.refresh(account, true);
|
|
272
|
+
return c.json({ ok: true });
|
|
273
|
+
}
|
|
274
|
+
case "stale.ask": {
|
|
275
|
+
if (!account)
|
|
276
|
+
throw new ActionError("No GitHub account.");
|
|
277
|
+
return c.json(await start(account, input.key, "stale"));
|
|
278
|
+
}
|
|
279
|
+
case "agent.send": {
|
|
280
|
+
const { key, text } = input;
|
|
281
|
+
agents.send(key, text);
|
|
282
|
+
return c.json({ ok: true });
|
|
283
|
+
}
|
|
284
|
+
case "agent.decide": {
|
|
285
|
+
const { key, approvalId, allow } = input;
|
|
286
|
+
agents.decide(key, approvalId, allow);
|
|
287
|
+
return c.json({ ok: true });
|
|
288
|
+
}
|
|
289
|
+
case "agent.handoff":
|
|
290
|
+
return c.json({ command: await agents.handOff(input.key) });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
/** Live updates for the home page and agent panels: "home" to refetch, "agent" with a session's new state. */
|
|
294
|
+
app.get("/events", (c) => streamSSE(c, async (stream) => {
|
|
295
|
+
let open = true;
|
|
296
|
+
let timer = null;
|
|
297
|
+
const onHome = () => {
|
|
298
|
+
if (timer || !open)
|
|
299
|
+
return;
|
|
300
|
+
timer = setTimeout(() => {
|
|
301
|
+
timer = null;
|
|
302
|
+
if (open)
|
|
303
|
+
void stream.writeSSE({ event: "home", data: "{}" });
|
|
304
|
+
}, 150);
|
|
305
|
+
};
|
|
306
|
+
const onAgent = (key) => {
|
|
307
|
+
const v = agents.view(key);
|
|
308
|
+
if (open && v)
|
|
309
|
+
void stream.writeSSE({ event: "agent", data: JSON.stringify(v) });
|
|
310
|
+
};
|
|
311
|
+
home.bus.on("changed", onHome);
|
|
312
|
+
agents.bus.on("changed", onAgent);
|
|
313
|
+
stream.onAbort(() => {
|
|
314
|
+
open = false;
|
|
315
|
+
home.bus.off("changed", onHome);
|
|
316
|
+
agents.bus.off("changed", onAgent);
|
|
317
|
+
});
|
|
318
|
+
while (open) {
|
|
319
|
+
await stream.sleep(25_000);
|
|
320
|
+
if (open)
|
|
321
|
+
void stream.writeSSE({ event: "ping", data: "{}" });
|
|
322
|
+
}
|
|
323
|
+
}));
|
|
324
|
+
return app;
|
|
325
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Where Downstream's own files live. A clone runs the TypeScript sources in
|
|
2
|
+
// place; a published copy (npx, npm install) sits under node_modules, where
|
|
3
|
+
// Node won't strip types, so it runs the JavaScript compiled into dist/.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const compiledExtension = ".js";
|
|
7
|
+
export const install = import.meta.url.endsWith(compiledExtension) ? "published" : "clone";
|
|
8
|
+
/** Holds package.json, SKILL.md, bin/ and web/dist/. */
|
|
9
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), install === "published" ? "../.." : "..");
|
|
10
|
+
/** Holds cli/, server/ and domain/ in the form Node runs. */
|
|
11
|
+
export const codeRoot = install === "published" ? path.join(packageRoot, "dist") : packageRoot;
|
|
12
|
+
export const serverEntry = path.join(codeRoot, "server", install === "published" ? "main.js" : "main.ts");
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// The Downstream server: one per machine. The home page lives at `/`; each
|
|
2
|
+
// repository's reviews at `/r/<repo>/`, each with its own database in the repo.
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { streamSSE } from "hono/streaming";
|
|
5
|
+
import { serve } from "@hono/node-server";
|
|
6
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
7
|
+
import { writeFileSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { actions, ActionError } from "./actions.js";
|
|
11
|
+
import { HomeStore, downstreamHome } from "./home-store.js";
|
|
12
|
+
import { RepoRegistry } from "./repos.js";
|
|
13
|
+
import { AgentHost } from "./agent-host.js";
|
|
14
|
+
import { homeRoutes } from "./home.js";
|
|
15
|
+
import { packageRoot } from "./install.js";
|
|
16
|
+
const args = Object.fromEntries(process.argv
|
|
17
|
+
.slice(2)
|
|
18
|
+
.map((a) => a.match(/^--([^=]+)=(.*)$/))
|
|
19
|
+
.filter((m) => !!m)
|
|
20
|
+
.map((m) => [m[1], m[2]]));
|
|
21
|
+
const port = Number(args.port ?? 4317);
|
|
22
|
+
const dir = downstreamHome();
|
|
23
|
+
const home = new HomeStore();
|
|
24
|
+
const repos = new RepoRegistry(home);
|
|
25
|
+
const agents = new AgentHost();
|
|
26
|
+
const actorOf = (h) => (h === "agent" ? "agent" : "user");
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
app.onError((err, c) => {
|
|
29
|
+
if (err instanceof ActionError)
|
|
30
|
+
return c.json({ error: err.message }, err.status);
|
|
31
|
+
if (err instanceof z.ZodError)
|
|
32
|
+
return c.json({ error: "Invalid input", issues: err.issues }, 400);
|
|
33
|
+
console.error(err);
|
|
34
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
35
|
+
});
|
|
36
|
+
const startedAt = Date.now();
|
|
37
|
+
app.get("/api/health", (c) => c.json({ ok: true, pid: process.pid, startedAt, home: dir }));
|
|
38
|
+
/** The CLI registers the repository it runs in and gets back the id its reviews live under. */
|
|
39
|
+
app.post("/api/repos", async (c) => {
|
|
40
|
+
const { root } = z.object({ root: z.string() }).parse(await c.req.json());
|
|
41
|
+
if (!existsSync(root))
|
|
42
|
+
throw new ActionError(`No such directory: ${root}`);
|
|
43
|
+
const repo = repos.register(root);
|
|
44
|
+
return c.json({ id: repo.id, root: repo.root, nameWithOwner: repo.nameWithOwner });
|
|
45
|
+
});
|
|
46
|
+
app.route("/api", homeRoutes({ home, repos, agents }));
|
|
47
|
+
// --- one repository's reviews ---------------------------------------------------
|
|
48
|
+
const review = new Hono();
|
|
49
|
+
review.use(async (c, next) => {
|
|
50
|
+
const repo = repos.get(c.req.param("repo") ?? "");
|
|
51
|
+
if (!repo)
|
|
52
|
+
return c.json({ error: "Unknown repository. Run `downstream open` inside it." }, 404);
|
|
53
|
+
c.set("repo", repo);
|
|
54
|
+
await next();
|
|
55
|
+
});
|
|
56
|
+
const reviewOf = (c, q) => q || c.var.repo.store.currentReviewId();
|
|
57
|
+
review.get("/reviews", (c) => c.json({ current: c.var.repo.store.currentReviewId(), reviews: c.var.repo.store.listReviews() }));
|
|
58
|
+
review.get("/state", (c) => {
|
|
59
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
60
|
+
if (!id)
|
|
61
|
+
return c.json({ error: "No review yet. Run `downstream open`." }, 404);
|
|
62
|
+
const state = c.var.repo.store.state(id);
|
|
63
|
+
return state ? c.json(state) : c.json({ error: `No review ${id}` }, 404);
|
|
64
|
+
});
|
|
65
|
+
review.get("/actions", (c) => c.json([...actions.values()].map((a) => ({ name: a.name, description: a.description, readOnly: !!a.readOnly, input: z.toJSONSchema(a.input, { io: "input", unrepresentable: "any" }) }))));
|
|
66
|
+
review.post("/actions/:name", async (c) => {
|
|
67
|
+
const def = actions.get(c.req.param("name"));
|
|
68
|
+
if (!def)
|
|
69
|
+
return c.json({ error: `Unknown action ${c.req.param("name")}. GET /api/actions lists them.` }, 404);
|
|
70
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
71
|
+
const actor = actorOf(c.req.header("x-downstream-actor"));
|
|
72
|
+
const repo = c.var.repo;
|
|
73
|
+
const reviewId = def.name === "review.open" ? "" : reviewOf(c, body.review);
|
|
74
|
+
if (def.name !== "review.open" && !reviewId)
|
|
75
|
+
throw new ActionError("No review yet. Run `downstream open` first.", 404);
|
|
76
|
+
if (actor === "agent")
|
|
77
|
+
repo.store.touchAgent();
|
|
78
|
+
const ctx = {
|
|
79
|
+
store: repo.store,
|
|
80
|
+
actor,
|
|
81
|
+
root: repo.root,
|
|
82
|
+
nameWithOwner: repo.nameWithOwner,
|
|
83
|
+
accountFor: (pr) => home.accountFor(repo.root, pr),
|
|
84
|
+
workspace: (id) => repo.workspace(id),
|
|
85
|
+
dropWorkspace: (id) => repo.dropWorkspace(id),
|
|
86
|
+
reviewId: reviewId ?? "",
|
|
87
|
+
};
|
|
88
|
+
const result = await def.run(ctx, def.input.parse(body.input ?? {}));
|
|
89
|
+
return c.json(result ?? { ok: true });
|
|
90
|
+
});
|
|
91
|
+
review.get("/frame", (c) => {
|
|
92
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
93
|
+
const symbol = c.var.repo.store.symbol(id, c.req.query("symbol") ?? "");
|
|
94
|
+
if (!symbol)
|
|
95
|
+
return c.json({ error: "No such symbol" }, 404);
|
|
96
|
+
const frame = c.var.repo.workspace(id).frame(symbol);
|
|
97
|
+
return frame ? c.json(frame) : c.json({ error: `Can't read ${symbol.file}` }, 404);
|
|
98
|
+
});
|
|
99
|
+
review.get("/file", (c) => {
|
|
100
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
101
|
+
const side = c.req.query("side") === "old" ? "old" : "new";
|
|
102
|
+
const text = c.var.repo.workspace(id).read(c.req.query("path") ?? "", side);
|
|
103
|
+
return text === null ? c.json({ error: "Not found" }, 404) : c.json({ text });
|
|
104
|
+
});
|
|
105
|
+
const pos = (c) => ({
|
|
106
|
+
file: c.req.query("file") ?? "",
|
|
107
|
+
line: Number(c.req.query("line")),
|
|
108
|
+
col: Number(c.req.query("col")),
|
|
109
|
+
side: (c.req.query("side") === "old" ? "old" : "new"),
|
|
110
|
+
});
|
|
111
|
+
review.get("/hover", (c) => {
|
|
112
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
113
|
+
const p = pos(c);
|
|
114
|
+
return c.json(c.var.repo.workspace(id).hover(p.file, p.line, p.col, p.side));
|
|
115
|
+
});
|
|
116
|
+
review.get("/definition", (c) => {
|
|
117
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
118
|
+
const p = pos(c);
|
|
119
|
+
return c.json(c.var.repo.workspace(id).definition(p.file, p.line, p.col, p.side, c.var.repo.store.symbols(id)));
|
|
120
|
+
});
|
|
121
|
+
review.get("/symbol-at", (c) => {
|
|
122
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
123
|
+
return c.json(c.var.repo.workspace(id).symbolAt(c.req.query("file") ?? "", Number(c.req.query("line"))));
|
|
124
|
+
});
|
|
125
|
+
/** Live sync for the browser: every event, plus presence changes. */
|
|
126
|
+
review.get("/events", (c) => {
|
|
127
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
128
|
+
const { store } = c.var.repo;
|
|
129
|
+
return streamSSE(c, async (stream) => {
|
|
130
|
+
let open = true;
|
|
131
|
+
const onEvent = (reviewId, e) => {
|
|
132
|
+
if (open && reviewId === id)
|
|
133
|
+
void stream.writeSSE({ event: "change", data: JSON.stringify(e), id: String(e.id) });
|
|
134
|
+
};
|
|
135
|
+
const onPresence = () => {
|
|
136
|
+
if (open)
|
|
137
|
+
void stream.writeSSE({ event: "presence", data: JSON.stringify(store.presence()) });
|
|
138
|
+
};
|
|
139
|
+
store.bus.on("event", onEvent);
|
|
140
|
+
store.bus.on("presence", onPresence);
|
|
141
|
+
onPresence();
|
|
142
|
+
stream.onAbort(() => {
|
|
143
|
+
open = false;
|
|
144
|
+
store.bus.off("event", onEvent);
|
|
145
|
+
store.bus.off("presence", onPresence);
|
|
146
|
+
});
|
|
147
|
+
while (open) {
|
|
148
|
+
await stream.sleep(20_000);
|
|
149
|
+
if (open)
|
|
150
|
+
onPresence();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* The agent's ear. Blocks until the user does something (replies, asks,
|
|
156
|
+
* resolves, gives a verdict, ends the session) and returns those events.
|
|
157
|
+
* While it blocks, the UI shows the agent as listening.
|
|
158
|
+
*/
|
|
159
|
+
const LISTEN_TO = new Set(["note.added", "note.replied", "note.status", "note.updated", "ask", "review.verdict", "review.done", "review.published", "symbol.annotated", "symbol.added", "edge.added", "steps.set"]);
|
|
160
|
+
review.get("/wait", async (c) => {
|
|
161
|
+
const { store } = c.var.repo;
|
|
162
|
+
const id = reviewOf(c, c.req.query("review"));
|
|
163
|
+
if (!id)
|
|
164
|
+
return c.json({ error: "No review" }, 404);
|
|
165
|
+
const after = Number(c.req.query("after") ?? store.cursor(id));
|
|
166
|
+
const timeout = Math.min(Number(c.req.query("timeout") ?? 600), 3600) * 1000;
|
|
167
|
+
const pending = () => store.events(id, after, "user").filter((e) => LISTEN_TO.has(e.type));
|
|
168
|
+
const ready = pending();
|
|
169
|
+
if (ready.length) {
|
|
170
|
+
store.markRead(id, ready);
|
|
171
|
+
return c.json({ events: ready, cursor: store.cursor(id), timedOut: false });
|
|
172
|
+
}
|
|
173
|
+
store.waiterJoined();
|
|
174
|
+
try {
|
|
175
|
+
const events = await new Promise((resolve) => {
|
|
176
|
+
let timer;
|
|
177
|
+
const onEvent = (reviewId, e) => {
|
|
178
|
+
if (reviewId !== id || e.actor !== "user" || !LISTEN_TO.has(e.type))
|
|
179
|
+
return;
|
|
180
|
+
// Let a burst (e.g. reply + resolve) land together.
|
|
181
|
+
setTimeout(() => finish(pending()), 400);
|
|
182
|
+
};
|
|
183
|
+
const finish = (evs) => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
store.bus.off("event", onEvent);
|
|
186
|
+
c.req.raw.signal.removeEventListener("abort", onAbort);
|
|
187
|
+
resolve(evs);
|
|
188
|
+
};
|
|
189
|
+
const onAbort = () => finish([]);
|
|
190
|
+
timer = setTimeout(() => finish([]), timeout);
|
|
191
|
+
store.bus.on("event", onEvent);
|
|
192
|
+
c.req.raw.signal.addEventListener("abort", onAbort);
|
|
193
|
+
});
|
|
194
|
+
store.markRead(id, events);
|
|
195
|
+
return c.json({ events, cursor: store.cursor(id), timedOut: events.length === 0 });
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
store.waiterLeft();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
app.route("/r/:repo/api", review);
|
|
202
|
+
const dist = path.join(packageRoot, "web", "dist");
|
|
203
|
+
if (existsSync(dist)) {
|
|
204
|
+
const rel = path.relative(process.cwd(), dist) || ".";
|
|
205
|
+
app.use("/*", serveStatic({ root: rel }));
|
|
206
|
+
// Read per request: a rebuilt UI points at new asset hashes, and a cached copy would load the old bundle.
|
|
207
|
+
app.get("*", (c) => c.html(readFileSync(path.join(dist, "index.html"), "utf8")));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
app.get("/", (c) => c.text("The web UI isn't built. Run `npm run build` in the downstream skill directory."));
|
|
211
|
+
}
|
|
212
|
+
const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
|
|
213
|
+
const url = `http://127.0.0.1:${info.port}`;
|
|
214
|
+
writeFileSync(path.join(dir, "server.json"), JSON.stringify({ url, port: info.port, pid: process.pid }, null, 2));
|
|
215
|
+
console.log(`downstream listening on ${url}`);
|
|
216
|
+
});
|
|
217
|
+
const shutdown = () => {
|
|
218
|
+
try {
|
|
219
|
+
agents.stopAll();
|
|
220
|
+
rmSync(path.join(dir, "server.json"), { force: true });
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
server.close();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
process.on("SIGINT", shutdown);
|
|
228
|
+
process.on("SIGTERM", shutdown);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The first message of an agent session Downstream starts, by the move you
|
|
2
|
+
// pressed. From there the agent drives Downstream (or the repo) as usual.
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { packageRoot } from "./install.js";
|
|
5
|
+
const skillDir = packageRoot;
|
|
6
|
+
/** What the agent is asked to do, by the move that was pressed. */
|
|
7
|
+
const PROMPTS = {
|
|
8
|
+
review: ({ number, repo }) => reviewPrompt(number, repo),
|
|
9
|
+
rereview: ({ number, repo }) => `${reviewPrompt(number, repo)} I reviewed it before; start from what changed since my last review.`,
|
|
10
|
+
"address-feedback": ({ number, url }) => `Pull request #${number} (${url}) has review feedback I haven't addressed. Read every unresolved thread and the review comments (\`gh pr view ${number} --comments\`, and the review threads through \`gh api graphql\`). Work in a git worktree on the PR's branch so my current checkout is untouched. For each thread, tell me what you'd change and why; change code only once I agree.`,
|
|
11
|
+
"fix-checks": ({ number, url }) => `Checks are failing on pull request #${number} (${url}). Find the failing check and its log (\`gh pr checks ${number}\`), reproduce it locally in a git worktree on the PR's branch so my current checkout is untouched, and propose the fix before you push anything.`,
|
|
12
|
+
"resolve-conflicts": ({ number, url }) => `Pull request #${number} (${url}) conflicts with its base. In a git worktree on its branch, rebase onto the base (it may be part of a \`gh stack\`; if so use \`gh stack rebase\`), and walk me through each conflict before resolving it. Don't push until I say so.`,
|
|
13
|
+
"finish-draft": ({ number, url }) => `Pull request #${number} (${url}) is still a draft. Read the PR and its branch and tell me, in a short list, what's left before it's ready for review. Don't change anything yet.`,
|
|
14
|
+
};
|
|
15
|
+
function reviewPrompt(number, repo) {
|
|
16
|
+
return [
|
|
17
|
+
`Review pull request #${number} of ${repo} with me in Downstream.`,
|
|
18
|
+
`Downstream is a skill at ${skillDir}: read ${path.join(skillDir, "SKILL.md")} first and follow it.`,
|
|
19
|
+
`Wherever it says \${CLAUDE_SKILL_DIR}, use ${skillDir}. Start with \`node ${path.join(skillDir, "bin", "downstream.mjs")} open --pr ${number}\`.`,
|
|
20
|
+
"You're running inside Downstream: I read the review and your replies in its margin, so talk to me there and keep `downstream wait` running between turns.",
|
|
21
|
+
].join(" ");
|
|
22
|
+
}
|
|
23
|
+
/** For a PR nobody has touched in weeks: a recommendation, with nothing posted. */
|
|
24
|
+
export function stalePrompt(item) {
|
|
25
|
+
const { number, url, role } = item.lead;
|
|
26
|
+
const options = role === "author" ? "ask the reviewers to look, close it, or leave it" : "remove myself as a reviewer, ask the author whether it's still going, review it, or leave it";
|
|
27
|
+
return `Pull request #${number} (${url}) has had no activity for weeks. Read the description, the conversation and the review threads (\`gh pr view ${number} --comments\`, review threads through \`gh api graphql\`) and tell me in a few lines where it stands and whether I should ${options}. Don't post, push or change anything.`;
|
|
28
|
+
}
|
|
29
|
+
export function promptFor(item) {
|
|
30
|
+
const make = PROMPTS[item.move.kind];
|
|
31
|
+
return make ? make({ number: item.lead.number, repo: item.lead.repo, url: item.lead.url }) : null;
|
|
32
|
+
}
|