@ayushporwal/coja 0.0.1
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/bin/coja.js +2 -0
- package/dist/ai/chat-handler.js +115 -0
- package/dist/ai/chats.js +92 -0
- package/dist/ai/history.js +70 -0
- package/dist/ai/prompt.js +73 -0
- package/dist/ai/providers.js +81 -0
- package/dist/ai/title.js +77 -0
- package/dist/ai/tools.js +495 -0
- package/dist/ai/types.js +22 -0
- package/dist/app.js +142 -0
- package/dist/cli.js +99 -0
- package/dist/codex/connection.js +293 -0
- package/dist/codex/errors.js +52 -0
- package/dist/codex/models.js +97 -0
- package/dist/codex/oauth.js +217 -0
- package/dist/codex/protocol.js +49 -0
- package/dist/codex/provider.js +209 -0
- package/dist/context.js +8 -0
- package/dist/custom-providers.js +134 -0
- package/dist/data-dir.js +25 -0
- package/dist/db.js +65 -0
- package/dist/forge/forge.js +11 -0
- package/dist/forge/gh-cli.js +99 -0
- package/dist/forge/github.js +627 -0
- package/dist/forge/graphql.js +106 -0
- package/dist/forge/queries.js +258 -0
- package/dist/git/plumbing.js +519 -0
- package/dist/git/remote.js +47 -0
- package/dist/git/run.js +256 -0
- package/dist/open-browser.js +27 -0
- package/dist/paths.js +18 -0
- package/dist/pr/fetcher.js +173 -0
- package/dist/pr/objects.js +83 -0
- package/dist/pr/refs.js +17 -0
- package/dist/projects/add.js +111 -0
- package/dist/projects/store.js +37 -0
- package/dist/routes/chat.js +187 -0
- package/dist/routes/fs.js +65 -0
- package/dist/routes/git.js +75 -0
- package/dist/routes/guard.js +96 -0
- package/dist/routes/http.js +35 -0
- package/dist/routes/projects.js +51 -0
- package/dist/routes/pulls.js +64 -0
- package/dist/routes/review.js +113 -0
- package/dist/routes/setup.js +285 -0
- package/dist/secrets/providers.js +27 -0
- package/dist/secrets/store.js +190 -0
- package/dist/server.js +51 -0
- package/dist/services.js +15 -0
- package/dist/shared/api.js +186 -0
- package/package.json +35 -0
- package/public/assets/abap-CLvhMVsD.js +1 -0
- package/public/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/public/assets/ada-C5qYipkI.js +1 -0
- package/public/assets/ahk-CA9gdOdF.js +1 -0
- package/public/assets/ahk2-_6rzW9BS.js +1 -0
- package/public/assets/andromeeda-vGVdxbeo.js +1 -0
- package/public/assets/angular-html-BvGMhoM4.js +1 -0
- package/public/assets/angular-ts-D4cJlfHk.js +1 -0
- package/public/assets/apache-U0d_L8uA.js +1 -0
- package/public/assets/apex-KFGFyKwG.js +1 -0
- package/public/assets/apl-BAzDJNhF.js +1 -0
- package/public/assets/applescript-CCn79oCD.js +1 -0
- package/public/assets/ara-4CJ0cIlV.js +1 -0
- package/public/assets/asciidoc-SCjQUq34.js +1 -0
- package/public/assets/asm-Cmm7eHzH.js +1 -0
- package/public/assets/astro-tbl65Kw6.js +1 -0
- package/public/assets/aurora-x-CDeNXAV0.js +1 -0
- package/public/assets/awk-BWXHIvNe.js +1 -0
- package/public/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/public/assets/ayu-light-C3h-C4tm.js +1 -0
- package/public/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/public/assets/ballerina-B7ZEbQpA.js +1 -0
- package/public/assets/bat-qdxAdBWQ.js +1 -0
- package/public/assets/beancount-D-usSTwE.js +1 -0
- package/public/assets/berry-DKpUyyne.js +1 -0
- package/public/assets/bibtex-Ci_nEsc7.js +1 -0
- package/public/assets/bicep-CUHmPFLl.js +1 -0
- package/public/assets/bird2-CYIUgYqB.js +1 -0
- package/public/assets/blade-Cy5u_P69.js +1 -0
- package/public/assets/bsl-_VjlB9mv.js +1 -0
- package/public/assets/c-xsYtGME5.js +1 -0
- package/public/assets/c3-D0apG41V.js +1 -0
- package/public/assets/cadence-CQ2zXKGN.js +1 -0
- package/public/assets/cairo-DLTphjLi.js +1 -0
- package/public/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/public/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/public/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/public/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/public/assets/chapel-DlXZg0HF.js +1 -0
- package/public/assets/clarity-CtfBBspg.js +1 -0
- package/public/assets/clojure-DqKBuwfJ.js +1 -0
- package/public/assets/cmake-Bj61d0ZC.js +1 -0
- package/public/assets/cobol-NNONwiOX.js +1 -0
- package/public/assets/codeowners-C8r90Shi.js +1 -0
- package/public/assets/codeql-oeQT6MSM.js +1 -0
- package/public/assets/coffee-O853MBTp.js +1 -0
- package/public/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/public/assets/coq-DuBRPxYg.js +1 -0
- package/public/assets/cpp-DtWY3seq.js +1 -0
- package/public/assets/crystal-CcZ3zkyx.js +1 -0
- package/public/assets/csharp-oqKa8noW.js +1 -0
- package/public/assets/css-mSNQLTOb.js +1 -0
- package/public/assets/csv-Dx-8-gkx.js +1 -0
- package/public/assets/cue-CE9AQfxI.js +1 -0
- package/public/assets/cypher-ClKdZ_lG.js +1 -0
- package/public/assets/d-qD-0Kul2.js +1 -0
- package/public/assets/dark-plus-Cs2F2srj.js +1 -0
- package/public/assets/dart-CnvKMtbv.js +1 -0
- package/public/assets/dax-BkyTk9wS.js +1 -0
- package/public/assets/desktop-Dlh5hvp9.js +1 -0
- package/public/assets/diff-woXpYk--.js +1 -0
- package/public/assets/docker-IyjqRm3v.js +1 -0
- package/public/assets/dotenv-_5a1GRtc.js +1 -0
- package/public/assets/dracula-BHWKrbxM.js +1 -0
- package/public/assets/dracula-soft-5eyTD99u.js +1 -0
- package/public/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/public/assets/edge-BA0HOmx0.js +1 -0
- package/public/assets/elixir-Buj0VxhK.js +1 -0
- package/public/assets/elm-lVH2iZsu.js +1 -0
- package/public/assets/emacs-lisp-c_oH4hRZ.js +1 -0
- package/public/assets/erb-CbQPls-2.js +1 -0
- package/public/assets/erlang-Cphh6RMH.js +1 -0
- package/public/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/public/assets/everforest-light-Df2xbC6M.js +1 -0
- package/public/assets/fennel-DQxkIbk2.js +1 -0
- package/public/assets/fish-BJitypiv.js +1 -0
- package/public/assets/fluent-C03EYrpw.js +1 -0
- package/public/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/public/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/public/assets/fsharp-D13ZGOAj.js +1 -0
- package/public/assets/gdresource-xMC7jr7x.js +1 -0
- package/public/assets/gdscript-DYvRD3hg.js +1 -0
- package/public/assets/gdshader-CBce3t8t.js +1 -0
- package/public/assets/genie-CV2tkWYe.js +1 -0
- package/public/assets/gherkin-DExj1W_8.js +1 -0
- package/public/assets/git-commit-BSykSTBG.js +1 -0
- package/public/assets/git-rebase-DidgtTB0.js +1 -0
- package/public/assets/github-dark-C-LZuMrd.js +1 -0
- package/public/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/public/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/public/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/public/assets/github-light-EUqPIrTm.js +1 -0
- package/public/assets/github-light-default-BXViO-2h.js +1 -0
- package/public/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/public/assets/gleam-CSRkHgEL.js +1 -0
- package/public/assets/glimmer-js-Cck5MdS-.js +1 -0
- package/public/assets/glimmer-ts-DMfQxyYr.js +1 -0
- package/public/assets/glsl-DPBFrb1h.js +1 -0
- package/public/assets/gn-ilITqXS6.js +1 -0
- package/public/assets/gnuplot-7GGW24-e.js +1 -0
- package/public/assets/go-rLFTqkRN.js +1 -0
- package/public/assets/graphql-BvPubeCr.js +1 -0
- package/public/assets/groovy-CacY0gHj.js +1 -0
- package/public/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/public/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/public/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/public/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/public/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/public/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/public/assets/hack-Drv5y81z.js +1 -0
- package/public/assets/haml-DcpINEk-.js +1 -0
- package/public/assets/handlebars-sj4UAwHW.js +1 -0
- package/public/assets/haskell-D8IpX4py.js +1 -0
- package/public/assets/haxe-C8oCnwJJ.js +1 -0
- package/public/assets/hcl-Dh228itO.js +1 -0
- package/public/assets/hjson-CxZEssPk.js +1 -0
- package/public/assets/hlsl-Cvrh5tZx.js +1 -0
- package/public/assets/horizon-CE9ld1lL.js +1 -0
- package/public/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/public/assets/houston-CsvMBhTu.js +1 -0
- package/public/assets/html-DQjDPZbg.js +1 -0
- package/public/assets/html-derivative-DscSYlel.js +1 -0
- package/public/assets/http-CCrfmrUy.js +1 -0
- package/public/assets/hurl-DLm6Xdy2.js +1 -0
- package/public/assets/hxml-Dcm88srW.js +1 -0
- package/public/assets/hy-CZbG8q4J.js +1 -0
- package/public/assets/imba-DsUTQ-LC.js +1 -0
- package/public/assets/index-BDRMh7Hy.js +2918 -0
- package/public/assets/index-Cw-82hD0.css +1 -0
- package/public/assets/ini-B5eOa1yu.js +1 -0
- package/public/assets/java-CzpiG9dn.js +1 -0
- package/public/assets/javascript-2eedadNh.js +1 -0
- package/public/assets/jinja-Cch1Cycs.js +1 -0
- package/public/assets/jison-C3DQW9A-.js +1 -0
- package/public/assets/json-BhBCwuxI.js +1 -0
- package/public/assets/json5-BR5RXkoi.js +1 -0
- package/public/assets/jsonc-CYpm1nAK.js +1 -0
- package/public/assets/jsonl-CmCQp5Yx.js +1 -0
- package/public/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/public/assets/jssm-DXw9l8Rf.js +1 -0
- package/public/assets/jsx-De1vU88o.js +1 -0
- package/public/assets/julia-BlJzw_Rq.js +1 -0
- package/public/assets/just-A2qsO4cM.js +1 -0
- package/public/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/public/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/public/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/public/assets/kdl-CsD5j6eV.js +1 -0
- package/public/assets/kotlin-DhhofPvG.js +1 -0
- package/public/assets/kusto-C7mF5XQf.js +1 -0
- package/public/assets/laserwave-C_8bwKvT.js +1 -0
- package/public/assets/latex-CDaOJk0y.js +1 -0
- package/public/assets/lean-CewbzKMR.js +1 -0
- package/public/assets/less-DVTAwKKz.js +1 -0
- package/public/assets/light-plus-DVQuIRkW.js +1 -0
- package/public/assets/liquid-BOnT7KYX.js +1 -0
- package/public/assets/llvm-DqHOMS-s.js +1 -0
- package/public/assets/log-BNLmms1o.js +1 -0
- package/public/assets/logo-Cluzi2Zq.js +1 -0
- package/public/assets/lottie-BT9QSO-G.js +2 -0
- package/public/assets/lua-CoLL9qBE.js +1 -0
- package/public/assets/luau-C2pBCSgt.js +1 -0
- package/public/assets/make-Dixweg8N.js +1 -0
- package/public/assets/markdown-BYOwaDjH.js +1 -0
- package/public/assets/marko-D8Xuri3Y.js +1 -0
- package/public/assets/material-theme-Bm3Qr25_.js +1 -0
- package/public/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/public/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/public/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/public/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/public/assets/matlab-D7qyCx1q.js +1 -0
- package/public/assets/mdc-BhVsbv7E.js +1 -0
- package/public/assets/mdx-DQZ5AkYe.js +1 -0
- package/public/assets/mermaid-BxpfTIzt.js +1 -0
- package/public/assets/min-dark-BSWPekZh.js +1 -0
- package/public/assets/min-light-DDpmG2fV.js +1 -0
- package/public/assets/mipsasm-BMqwQI7S.js +1 -0
- package/public/assets/mojo-fg40_ntQ.js +1 -0
- package/public/assets/monokai-B_bV9PQ9.js +1 -0
- package/public/assets/moonbit-DgjMoLWr.js +1 -0
- package/public/assets/move-BYnGhZoM.js +1 -0
- package/public/assets/narrat-_X_XdTYD.js +1 -0
- package/public/assets/nextflow-BJtWHP5T.js +1 -0
- package/public/assets/nextflow-groovy-DJMQeKeT.js +1 -0
- package/public/assets/nginx-CN1bxnHs.js +1 -0
- package/public/assets/night-owl-DhmEMT88.js +1 -0
- package/public/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/public/assets/nim-BKDc-Eam.js +1 -0
- package/public/assets/nix-IvuFDN5E.js +1 -0
- package/public/assets/nord-Cb4Vim4T.js +1 -0
- package/public/assets/nsis-CjNLYCLg.js +1 -0
- package/public/assets/nushell-DHYfO66s.js +1 -0
- package/public/assets/objective-c-D1A_Heim.js +1 -0
- package/public/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/public/assets/ocaml-O90oeIOV.js +1 -0
- package/public/assets/odin-B1RWQWA5.js +1 -0
- package/public/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/public/assets/one-light-D7Lr4KcI.js +1 -0
- package/public/assets/openscad-BUDT5pXO.js +1 -0
- package/public/assets/org-CBldiceC.js +1 -0
- package/public/assets/pascal-4ZHwLPI5.js +1 -0
- package/public/assets/perl-KiFGXvwB.js +1 -0
- package/public/assets/php-CjFYgW_Q.js +1 -0
- package/public/assets/pierre-dark-CpLgRqie.js +1 -0
- package/public/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/public/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/public/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/public/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/public/assets/pierre-light-CoaEpmwp.js +1 -0
- package/public/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/public/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/public/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/public/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/public/assets/pkl-ot-7Btpt.js +1 -0
- package/public/assets/plastic-DQwYfKfQ.js +1 -0
- package/public/assets/plsql-DGHpHOYJ.js +1 -0
- package/public/assets/po-BiJDBrnU.js +1 -0
- package/public/assets/poimandres-DRFjx7u4.js +1 -0
- package/public/assets/polar-C7UOKdEL.js +1 -0
- package/public/assets/postcss-BXeXVLqQ.js +1 -0
- package/public/assets/powerquery-DNMTfnFr.js +1 -0
- package/public/assets/powershell-CCSX75yM.js +1 -0
- package/public/assets/prisma-DMgNhnYS.js +1 -0
- package/public/assets/prolog-iXnhIJG7.js +1 -0
- package/public/assets/proto-DB4EqR-F.js +1 -0
- package/public/assets/pug-CiQNzZtF.js +1 -0
- package/public/assets/puppet-CDv2pdJW.js +1 -0
- package/public/assets/purescript-9MfHhQsQ.js +1 -0
- package/public/assets/python-gzcpVVnB.js +1 -0
- package/public/assets/qml-BynhyZxj.js +1 -0
- package/public/assets/qmldir-DCQb3MpD.js +1 -0
- package/public/assets/qss-Fe1Jh2GI.js +1 -0
- package/public/assets/r-BydfSESc.js +1 -0
- package/public/assets/racket-DcIDlBhZ.js +1 -0
- package/public/assets/raku-B3gFvitq.js +1 -0
- package/public/assets/razor-Bfi-c6ag.js +1 -0
- package/public/assets/rbs-C0MpOrj4.js +1 -0
- package/public/assets/red-DX1BQOs8.js +1 -0
- package/public/assets/reg-CRGYupPL.js +1 -0
- package/public/assets/regexp-BFpups_T.js +1 -0
- package/public/assets/rel-BtDbiS_P.js +1 -0
- package/public/assets/riscv-Ckw8ddFX.js +1 -0
- package/public/assets/ron-VUp2lXgN.js +1 -0
- package/public/assets/rose-pine-aMsaMD1D.js +1 -0
- package/public/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/public/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/public/assets/rosmsg-CAekHB0j.js +1 -0
- package/public/assets/rst-Z5_5o5cf.js +1 -0
- package/public/assets/ruby-DyP6QZ5x.js +1 -0
- package/public/assets/rust-Cfkwpbl8.js +1 -0
- package/public/assets/sas-DzcEtbFW.js +1 -0
- package/public/assets/sass-DXrisJhu.js +1 -0
- package/public/assets/scala-BiFt_7mu.js +1 -0
- package/public/assets/scheme-DQCgrYNe.js +1 -0
- package/public/assets/scss-BVi50TCh.js +1 -0
- package/public/assets/sdbl-bTVj8UrX.js +1 -0
- package/public/assets/shaderlab-TOUzSsQk.js +1 -0
- package/public/assets/shellscript-DwBEbT4l.js +1 -0
- package/public/assets/shellsession-DtCkQKX0.js +1 -0
- package/public/assets/slack-dark-DnToyrRv.js +1 -0
- package/public/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/public/assets/smalltalk-BHpQb5nM.js +1 -0
- package/public/assets/smithy-Dgaz7aur.js +1 -0
- package/public/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/public/assets/solarized-dark-UdsmMDCL.js +1 -0
- package/public/assets/solarized-light-C1IUL_tW.js +1 -0
- package/public/assets/solidity-Bj7E1mRn.js +1 -0
- package/public/assets/soy-Col_eH-0.js +1 -0
- package/public/assets/sparql-D_iOobhT.js +1 -0
- package/public/assets/splunk-BC2Px7Mm.js +1 -0
- package/public/assets/sql-CF1geVQJ.js +1 -0
- package/public/assets/ssh-config-BgfXC-Er.js +1 -0
- package/public/assets/stata-JT7HPvq1.js +1 -0
- package/public/assets/stylus-B6D30XZt.js +1 -0
- package/public/assets/surrealql-kYIUYZug.js +1 -0
- package/public/assets/svelte-CsWZn_FS.js +1 -0
- package/public/assets/swift-CyEgAFGc.js +1 -0
- package/public/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/public/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/public/assets/systemd-BxMlprV5.js +1 -0
- package/public/assets/talonscript-CohzipZa.js +1 -0
- package/public/assets/tasl-DMoTqEGO.js +1 -0
- package/public/assets/tcl-CZd0xW_V.js +1 -0
- package/public/assets/templ-DCsT8WEJ.js +1 -0
- package/public/assets/terraform-DswuEJGm.js +1 -0
- package/public/assets/tex-ZyN9TW16.js +1 -0
- package/public/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/public/assets/toml-CcmNWLt0.js +1 -0
- package/public/assets/ts-tags-ArLnwaTi.js +1 -0
- package/public/assets/tsv-sltzmVWM.js +1 -0
- package/public/assets/tsx-CXwa5__r.js +1 -0
- package/public/assets/turtle-ByJddavk.js +1 -0
- package/public/assets/twig-BsBKj8BU.js +1 -0
- package/public/assets/typescript-Brm8t4pO.js +1 -0
- package/public/assets/typespec-BRdr0IET.js +1 -0
- package/public/assets/typst-vNjrO6QE.js +1 -0
- package/public/assets/v-DBj4K9d_.js +1 -0
- package/public/assets/vala-zf12oZj6.js +1 -0
- package/public/assets/vb-DOwzaHZ0.js +1 -0
- package/public/assets/verilog-B2gwz-sJ.js +1 -0
- package/public/assets/vesper-DdrHHSXu.js +1 -0
- package/public/assets/vhdl-BroJfC0k.js +1 -0
- package/public/assets/viml-DvXPmvsu.js +1 -0
- package/public/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/public/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/public/assets/vitesse-light-VbXTXTou.js +1 -0
- package/public/assets/vue-CcPq89qx.js +1 -0
- package/public/assets/vue-html-B5qW2N7E.js +1 -0
- package/public/assets/vue-vine-LYElP1mf.js +1 -0
- package/public/assets/vyper-CgoNMtux.js +1 -0
- package/public/assets/wasm-BnjxR4X6.js +1 -0
- package/public/assets/wasm-ByWQv1Qj.js +1 -0
- package/public/assets/wenyan-C8pVoKbM.js +1 -0
- package/public/assets/wgsl-BsKzXJz4.js +1 -0
- package/public/assets/wikitext-ClFFjSW2.js +1 -0
- package/public/assets/wit-DdvCle-K.js +1 -0
- package/public/assets/wolfram-DLL8P-h_.js +1 -0
- package/public/assets/xml-D5hvN78Q.js +1 -0
- package/public/assets/xsl-CBSZ35CX.js +1 -0
- package/public/assets/yaml-D4FTZq40.js +1 -0
- package/public/assets/zenscript-BnlCZFoB.js +1 -0
- package/public/assets/zig-CMLA9XwU.js +1 -0
- package/public/brand/coja/cj-dark.svg +1 -0
- package/public/brand/coja/cj-light.svg +1 -0
- package/public/brand/coja/cj-themeable.svg +1 -0
- package/public/brand/coja/lottie/cj-huge-dark.json +311 -0
- package/public/brand/coja/lottie/cj-huge-light.json +311 -0
- package/public/brand/coja/lottie/cj-large-dark.json +311 -0
- package/public/brand/coja/lottie/cj-large-light.json +311 -0
- package/public/brand/coja/lottie/cj-medium-dark.json +311 -0
- package/public/brand/coja/lottie/cj-medium-light.json +311 -0
- package/public/brand/coja/lottie/cj-small-dark.json +311 -0
- package/public/brand/coja/lottie/cj-small-light.json +311 -0
- package/public/brand/coja/manifest.json +172 -0
- package/public/brand/coja/themes/ember/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/ember/dark/cj.svg +1 -0
- package/public/brand/coja/themes/ember/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-large.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/ember/light/cj-small.json +311 -0
- package/public/brand/coja/themes/ember/light/cj.svg +1 -0
- package/public/brand/coja/themes/grove/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/grove/dark/cj.svg +1 -0
- package/public/brand/coja/themes/grove/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-large.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/grove/light/cj-small.json +311 -0
- package/public/brand/coja/themes/grove/light/cj.svg +1 -0
- package/public/brand/coja/themes/iris/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/iris/dark/cj.svg +1 -0
- package/public/brand/coja/themes/iris/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-large.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/iris/light/cj-small.json +311 -0
- package/public/brand/coja/themes/iris/light/cj.svg +1 -0
- package/public/brand/coja/themes/mulberry/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/mulberry/dark/cj.svg +1 -0
- package/public/brand/coja/themes/mulberry/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-large.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj-small.json +311 -0
- package/public/brand/coja/themes/mulberry/light/cj.svg +1 -0
- package/public/brand/coja/themes/ocean/dark/cj-huge.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-large.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-medium.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj-small.json +311 -0
- package/public/brand/coja/themes/ocean/dark/cj.svg +1 -0
- package/public/brand/coja/themes/ocean/light/cj-huge.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-large.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-medium.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj-small.json +311 -0
- package/public/brand/coja/themes/ocean/light/cj.svg +1 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Bold.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Italic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Medium.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-MediumItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-Regular.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.woff2 +0 -0
- package/public/fonts/ibm-plex-sans/LICENSE.txt +93 -0
- package/public/fonts/ibm-plex-sans/PROVENANCE.md +10 -0
- package/public/fonts/maple-mono-nf/LICENSE.txt +93 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Bold.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-BoldItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Italic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Medium.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-MediumItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-Regular.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBold.woff2 +0 -0
- package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBoldItalic.woff2 +0 -0
- package/public/fonts/maple-mono-nf/PROVENANCE.md +12 -0
- package/public/index.html +14 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { assertGitVersion } from './git/plumbing.js';
|
|
3
|
+
import { openBrowser } from './open-browser.js';
|
|
4
|
+
import { packageVersion, resolvePublicDir } from './paths.js';
|
|
5
|
+
import { startServer } from './server.js';
|
|
6
|
+
import { createServices } from './services.js';
|
|
7
|
+
const DEFAULT_PORT = 4321;
|
|
8
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
9
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
10
|
+
/**
|
|
11
|
+
* Unspecified addresses. The request guard compares every Host header against
|
|
12
|
+
* loopback and the `--host` value, and no header ever equals a wildcard, so a
|
|
13
|
+
* server bound this way would refuse every request. Refused at parse time.
|
|
14
|
+
*/
|
|
15
|
+
const WILDCARD_HOSTS = new Set(['0.0.0.0', '::', '[::]', '0:0:0:0:0:0:0:0', '']);
|
|
16
|
+
const HELP = `coja — local code review for GitHub pull requests
|
|
17
|
+
|
|
18
|
+
Usage: coja [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-p, --port <n> Port to listen on (default ${DEFAULT_PORT}; a free port is picked if it is taken)
|
|
22
|
+
--host <h> Address to bind (default ${DEFAULT_HOST}; coja is meant to stay on loopback;
|
|
23
|
+
wildcards such as 0.0.0.0 or :: are refused)
|
|
24
|
+
--no-open Do not open the browser
|
|
25
|
+
-v, --version Print the version and exit
|
|
26
|
+
-h, --help Show this help and exit
|
|
27
|
+
`;
|
|
28
|
+
function parseCli(argv) {
|
|
29
|
+
const { values } = parseArgs({
|
|
30
|
+
args: argv,
|
|
31
|
+
allowNegative: true,
|
|
32
|
+
options: {
|
|
33
|
+
port: { type: 'string', short: 'p', default: String(DEFAULT_PORT) },
|
|
34
|
+
host: { type: 'string', default: DEFAULT_HOST },
|
|
35
|
+
open: { type: 'boolean', default: true },
|
|
36
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
37
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const port = Number(values.port);
|
|
41
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
42
|
+
throw new Error(`invalid --port "${values.port}": expected an integer between 0 and 65535`);
|
|
43
|
+
}
|
|
44
|
+
const host = values.host.trim();
|
|
45
|
+
if (WILDCARD_HOSTS.has(host)) {
|
|
46
|
+
throw new Error(`--host "${values.host}" is a wildcard bind, which the request guard could never match (no Host header equals it): ` +
|
|
47
|
+
'bind to a concrete address, e.g. --host 192.168.1.5; coja is designed for loopback');
|
|
48
|
+
}
|
|
49
|
+
return { port, host, open: values.open, version: values.version, help: values.help };
|
|
50
|
+
}
|
|
51
|
+
async function main() {
|
|
52
|
+
let cli;
|
|
53
|
+
try {
|
|
54
|
+
cli = parseCli(process.argv.slice(2));
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error(`coja: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
58
|
+
console.error(HELP);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
if (cli.help) {
|
|
62
|
+
console.log(HELP);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (cli.version) {
|
|
66
|
+
console.log(packageVersion());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!LOOPBACK_HOSTS.has(cli.host)) {
|
|
70
|
+
console.warn(`coja: binding to ${cli.host} exposes the server beyond this machine; use loopback unless you know why.`);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await assertGitVersion();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(`coja: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const services = await createServices();
|
|
80
|
+
const server = await startServer({
|
|
81
|
+
port: cli.port,
|
|
82
|
+
host: cli.host,
|
|
83
|
+
publicDir: resolvePublicDir(),
|
|
84
|
+
services,
|
|
85
|
+
allowedHosts: LOOPBACK_HOSTS.has(cli.host) ? [] : [cli.host],
|
|
86
|
+
});
|
|
87
|
+
if (cli.port !== 0 && server.port !== cli.port) {
|
|
88
|
+
console.log(`coja: port ${cli.port} is in use, using ${server.port} instead`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`coja v${packageVersion()} → ${server.url}`);
|
|
91
|
+
if (cli.open)
|
|
92
|
+
openBrowser(server.url);
|
|
93
|
+
const shutdown = () => {
|
|
94
|
+
server.close().finally(() => process.exit(0));
|
|
95
|
+
};
|
|
96
|
+
process.once('SIGINT', shutdown);
|
|
97
|
+
process.once('SIGTERM', shutdown);
|
|
98
|
+
}
|
|
99
|
+
await main();
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { settings } from '../db.js';
|
|
2
|
+
import { CodexError } from './errors.js';
|
|
3
|
+
import { fetchModelCatalog } from './models.js';
|
|
4
|
+
import { extractMetadata, refreshAccessToken, startBrowserFlow, } from './oauth.js';
|
|
5
|
+
import { CODEX_PROTOCOL } from './protocol.js';
|
|
6
|
+
/**
|
|
7
|
+
* The single ChatGPT connection of this coja instance. Tokens live only in
|
|
8
|
+
* the SecretStore (OS keychain) under one key; the browser never receives
|
|
9
|
+
* anything but connection status (connected, email, plan). Refreshes are
|
|
10
|
+
* singleflight: concurrent turns join one in-flight refresh, and a terminal
|
|
11
|
+
* refresh failure marks the connection auth-expired so later calls fail fast
|
|
12
|
+
* without network until the user reconnects.
|
|
13
|
+
*/
|
|
14
|
+
export const CHATGPT_AUTH_KEY = 'chatgpt-auth';
|
|
15
|
+
/** Settings mirror of the last good model catalog (non-secret) for offline fallbacks. */
|
|
16
|
+
export const CHATGPT_CATALOG_KEY = 'chatgpt-model-catalog';
|
|
17
|
+
export const CATALOG_TTL_MS = 10 * 60 * 1000;
|
|
18
|
+
export class ChatGptConnection {
|
|
19
|
+
secrets;
|
|
20
|
+
protocol;
|
|
21
|
+
fetchImpl;
|
|
22
|
+
openBrowser;
|
|
23
|
+
/** Read-through cache so chat turns do not hit the keychain on every call. */
|
|
24
|
+
cached;
|
|
25
|
+
refreshPromise = null;
|
|
26
|
+
flow = null;
|
|
27
|
+
catalog = null;
|
|
28
|
+
catalogExpiresAt = 0;
|
|
29
|
+
catalogPromise = null;
|
|
30
|
+
db;
|
|
31
|
+
catalogTtlMs;
|
|
32
|
+
authExpired = null;
|
|
33
|
+
connectError = null;
|
|
34
|
+
constructor(deps) {
|
|
35
|
+
this.secrets = deps.secrets;
|
|
36
|
+
this.db = deps.db;
|
|
37
|
+
this.catalogTtlMs = deps.catalogTtlMs ?? CATALOG_TTL_MS;
|
|
38
|
+
this.protocol = deps.protocol ?? CODEX_PROTOCOL;
|
|
39
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
40
|
+
this.openBrowser =
|
|
41
|
+
deps.openBrowser ??
|
|
42
|
+
((url) => {
|
|
43
|
+
void import('../open-browser.js').then((m) => m.openBrowser(url));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Status for the browser: never token material, just connection facts. */
|
|
47
|
+
async status() {
|
|
48
|
+
const signingIn = this.flow !== null;
|
|
49
|
+
const record = await this.load();
|
|
50
|
+
if (!record) {
|
|
51
|
+
return {
|
|
52
|
+
connected: false,
|
|
53
|
+
signingIn,
|
|
54
|
+
...(this.connectError ? { connectError: this.connectError } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
connected: true,
|
|
59
|
+
signingIn,
|
|
60
|
+
...(record.email ? { email: record.email } : {}),
|
|
61
|
+
...(record.planType ? { plan: record.planType } : {}),
|
|
62
|
+
...(this.authExpired ? { authExpired: true } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Start the browser authorization flow. Resolves fast with the URL the
|
|
67
|
+
* browser should open (the server also opens it); the flow completes in the
|
|
68
|
+
* background and `status()` turns connected once the user approves.
|
|
69
|
+
*/
|
|
70
|
+
async connect() {
|
|
71
|
+
if (this.flow)
|
|
72
|
+
return { authUrl: this.flow.authUrl };
|
|
73
|
+
this.connectError = null;
|
|
74
|
+
let flow;
|
|
75
|
+
try {
|
|
76
|
+
flow = await startBrowserFlow({
|
|
77
|
+
protocol: this.protocol,
|
|
78
|
+
fetchImpl: this.fetchImpl,
|
|
79
|
+
openBrowser: this.openBrowser,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.connectError = err instanceof Error ? err.message : String(err);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
this.flow = flow;
|
|
87
|
+
flow.completion.then((tokens) => {
|
|
88
|
+
if (this.flow === flow)
|
|
89
|
+
this.flow = null;
|
|
90
|
+
return this.completeLogin(tokens);
|
|
91
|
+
}, (err) => {
|
|
92
|
+
if (this.flow === flow)
|
|
93
|
+
this.flow = null;
|
|
94
|
+
// A cancelled/timed-out login is a Setup-screen fact, not a chat error.
|
|
95
|
+
this.connectError = err instanceof Error ? err.message : String(err);
|
|
96
|
+
});
|
|
97
|
+
return { authUrl: flow.authUrl };
|
|
98
|
+
}
|
|
99
|
+
/** Forget tokens and any pending sign-in. There is no dependable upstream revoke. */
|
|
100
|
+
async disconnect() {
|
|
101
|
+
this.flow?.cancel('disconnected');
|
|
102
|
+
this.flow = null;
|
|
103
|
+
this.cached = null;
|
|
104
|
+
this.authExpired = null;
|
|
105
|
+
this.connectError = null;
|
|
106
|
+
this.refreshPromise = null;
|
|
107
|
+
await this.secrets.delete(CHATGPT_AUTH_KEY);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A fresh access token for a chat turn, refreshing inside the margin.
|
|
111
|
+
* Throws the typed `auth_expired` without network once the connection is
|
|
112
|
+
* marked expired.
|
|
113
|
+
*/
|
|
114
|
+
async getFreshAccessToken() {
|
|
115
|
+
if (this.authExpired)
|
|
116
|
+
throw new CodexError('auth_expired', this.authExpired.reason);
|
|
117
|
+
const record = await this.requireRecord();
|
|
118
|
+
if (record.expiresAt - Date.now() >= this.protocol.refreshMarginMs) {
|
|
119
|
+
return this.fresh(record);
|
|
120
|
+
}
|
|
121
|
+
return this.singleflightRefresh(false);
|
|
122
|
+
}
|
|
123
|
+
/** Force a refresh (singleflight); used by the transport after a 401. */
|
|
124
|
+
async forceRefresh() {
|
|
125
|
+
if (this.authExpired)
|
|
126
|
+
throw new CodexError('auth_expired', this.authExpired.reason);
|
|
127
|
+
await this.requireRecord();
|
|
128
|
+
return this.singleflightRefresh(true);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The backend's model catalog for this account, cached for the TTL and
|
|
132
|
+
* mirrored (non-secret) in the settings DB: when the endpoint is unreachable
|
|
133
|
+
* the mirror keeps the picker alive. Shared singleflight so concurrent
|
|
134
|
+
* pickers do one fetch.
|
|
135
|
+
*/
|
|
136
|
+
async listModels() {
|
|
137
|
+
if (this.catalog && this.catalogExpiresAt > Date.now())
|
|
138
|
+
return this.catalog;
|
|
139
|
+
if (!this.catalogPromise) {
|
|
140
|
+
this.catalogPromise = this.doFetchCatalog().finally(() => {
|
|
141
|
+
this.catalogPromise = null;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return this.catalogPromise;
|
|
145
|
+
}
|
|
146
|
+
/** The catalog's default effort for a model, from the cached catalog only (best effort). */
|
|
147
|
+
defaultReasoningEffortFor(modelId) {
|
|
148
|
+
return this.catalog?.find((m) => m.slug === modelId)?.defaultReasoningEffort;
|
|
149
|
+
}
|
|
150
|
+
/** Whether `effort` is in the model's catalog entry; fetches the catalog when stale. */
|
|
151
|
+
async supportsReasoningEffort(modelId, effort) {
|
|
152
|
+
const models = await this.listModels();
|
|
153
|
+
const entry = models.find((m) => m.slug === modelId);
|
|
154
|
+
return entry ? entry.reasoningEfforts.some((level) => level.effort === effort) : false;
|
|
155
|
+
}
|
|
156
|
+
async doFetchCatalog() {
|
|
157
|
+
try {
|
|
158
|
+
const models = await fetchModelCatalog({
|
|
159
|
+
fetchImpl: this.fetchImpl,
|
|
160
|
+
protocol: this.protocol,
|
|
161
|
+
getToken: () => this.getFreshAccessToken(),
|
|
162
|
+
forceRefresh: () => this.forceRefresh(),
|
|
163
|
+
});
|
|
164
|
+
this.catalog = models;
|
|
165
|
+
this.catalogExpiresAt = Date.now() + this.catalogTtlMs;
|
|
166
|
+
this.mirrorCatalog(models);
|
|
167
|
+
return models;
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const mirrored = this.mirroredCatalog();
|
|
171
|
+
if (mirrored) {
|
|
172
|
+
this.catalog = mirrored;
|
|
173
|
+
this.catalogExpiresAt = Date.now() + this.catalogTtlMs;
|
|
174
|
+
return mirrored;
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
mirrorCatalog(models) {
|
|
180
|
+
if (!this.db)
|
|
181
|
+
return;
|
|
182
|
+
try {
|
|
183
|
+
settings.set(this.db, CHATGPT_CATALOG_KEY, JSON.stringify(models));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// A mirror write must never break the happy path.
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
mirroredCatalog() {
|
|
190
|
+
if (!this.db)
|
|
191
|
+
return null;
|
|
192
|
+
try {
|
|
193
|
+
const raw = settings.get(this.db, CHATGPT_CATALOG_KEY);
|
|
194
|
+
return raw ? JSON.parse(raw) : null;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// -------------------------------------------------------------------------
|
|
201
|
+
fresh(record) {
|
|
202
|
+
return {
|
|
203
|
+
accessToken: record.accessToken,
|
|
204
|
+
...(record.accountId ? { accountId: record.accountId } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async requireRecord() {
|
|
208
|
+
const record = await this.load();
|
|
209
|
+
if (!record) {
|
|
210
|
+
throw new CodexError('auth_expired', 'ChatGPT is not connected — connect it in Setup.');
|
|
211
|
+
}
|
|
212
|
+
return record;
|
|
213
|
+
}
|
|
214
|
+
singleflightRefresh(force) {
|
|
215
|
+
if (!this.refreshPromise) {
|
|
216
|
+
this.refreshPromise = this.doRefresh(force).finally(() => {
|
|
217
|
+
this.refreshPromise = null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return this.refreshPromise;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The actual refresh. Reloads the record inside the promise (another turn
|
|
224
|
+
* may have refreshed while this caller waited) and skips the margin re-check
|
|
225
|
+
* only when the caller forces (a 401 retry). A forced refresh joins an
|
|
226
|
+
* in-flight regular one and vice versa — one HTTP call either way. The old
|
|
227
|
+
* refresh token is kept when the response omits one. Auth-expiry is marked
|
|
228
|
+
* only on terminal failures — OAuth `invalid_*` or a bare 401/403, which
|
|
229
|
+
* `tokenRequest` classifies; 429/5xx/network stay retryable.
|
|
230
|
+
*/
|
|
231
|
+
async doRefresh(force) {
|
|
232
|
+
const current = await this.load();
|
|
233
|
+
if (!current) {
|
|
234
|
+
throw new CodexError('auth_expired', 'ChatGPT is not connected — connect it in Setup.');
|
|
235
|
+
}
|
|
236
|
+
if (!force && current.expiresAt - Date.now() >= this.protocol.refreshMarginMs) {
|
|
237
|
+
// Another refresh landed while this caller waited for the singleflight.
|
|
238
|
+
return this.fresh(current);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const tokens = await refreshAccessToken({ fetchImpl: this.fetchImpl, protocol: this.protocol }, current.refreshToken);
|
|
242
|
+
const next = {
|
|
243
|
+
...current,
|
|
244
|
+
accessToken: tokens.access_token,
|
|
245
|
+
...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}),
|
|
246
|
+
...(tokens.id_token ? { idToken: tokens.id_token } : {}),
|
|
247
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
248
|
+
...extractMetadata(tokens),
|
|
249
|
+
};
|
|
250
|
+
await this.save(next);
|
|
251
|
+
this.authExpired = null;
|
|
252
|
+
return this.fresh(next);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (err instanceof CodexError && err.code === 'auth_expired') {
|
|
256
|
+
this.authExpired = { reason: err.message };
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async completeLogin(tokens) {
|
|
262
|
+
const next = {
|
|
263
|
+
accessToken: tokens.access_token,
|
|
264
|
+
refreshToken: tokens.refresh_token ?? '',
|
|
265
|
+
...(tokens.id_token ? { idToken: tokens.id_token } : {}),
|
|
266
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
267
|
+
...extractMetadata(tokens),
|
|
268
|
+
};
|
|
269
|
+
await this.save(next);
|
|
270
|
+
this.authExpired = null;
|
|
271
|
+
this.connectError = null;
|
|
272
|
+
}
|
|
273
|
+
async load() {
|
|
274
|
+
if (this.cached !== undefined)
|
|
275
|
+
return this.cached;
|
|
276
|
+
const raw = await this.secrets.get(CHATGPT_AUTH_KEY);
|
|
277
|
+
if (!raw) {
|
|
278
|
+
this.cached = null;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
this.cached = JSON.parse(raw);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
this.cached = null;
|
|
286
|
+
}
|
|
287
|
+
return this.cached;
|
|
288
|
+
}
|
|
289
|
+
async save(record) {
|
|
290
|
+
await this.secrets.set(CHATGPT_AUTH_KEY, JSON.stringify(record));
|
|
291
|
+
this.cached = record;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CHATGPT_MARKER_PATTERN, CHATGPT_MARKER_PREFIX, markChatGptError, } from '../shared/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Typed failures of the ChatGPT subscription path. Every error carries a
|
|
4
|
+
* stable `code` (classification is never done by matching message text), and
|
|
5
|
+
* its message is written for the reviewer — it may be shown in the chat panel
|
|
6
|
+
* verbatim, so it names what happened and what to do, never token material.
|
|
7
|
+
*
|
|
8
|
+
* The message self-marks with the `[coja:chatgpt:<code>]` marker: chat-turn
|
|
9
|
+
* errors reach the browser as bare strings (the AI SDK stream carries error
|
|
10
|
+
* text only), and the marker is how the chat panel recognises a subscription
|
|
11
|
+
* failure and offers reconnect / API-key actions without the chat handler
|
|
12
|
+
* knowing anything about billing paths.
|
|
13
|
+
*/
|
|
14
|
+
const HEADINGS = {
|
|
15
|
+
state_mismatch: 'The ChatGPT sign-in could not be verified. Start it again.',
|
|
16
|
+
auth_expired: 'Your ChatGPT sign-in has expired. Reconnect ChatGPT in Setup.',
|
|
17
|
+
quota: 'Your ChatGPT plan limit is reached.',
|
|
18
|
+
endpoint_changed: 'The ChatGPT subscription backend refused the request. Use your OpenAI API key, and check for a coja update if this keeps happening.',
|
|
19
|
+
transport: 'Could not reach the ChatGPT subscription backend.',
|
|
20
|
+
};
|
|
21
|
+
export class CodexError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
extra;
|
|
24
|
+
constructor(code, detail, extra) {
|
|
25
|
+
const heading = HEADINGS[code];
|
|
26
|
+
super(markChatGptError(code, detail ? `${heading} (${detail})` : heading));
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.extra = extra;
|
|
29
|
+
this.name = 'CodexError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export { CHATGPT_MARKER_PATTERN, CHATGPT_MARKER_PREFIX };
|
|
33
|
+
/**
|
|
34
|
+
* Redact token material before truncation (PROTOCOL.md §10): a bearer header
|
|
35
|
+
* or a JSON/form field carrying a token never survives into a thrown message
|
|
36
|
+
* or log line.
|
|
37
|
+
*/
|
|
38
|
+
export function redact(text) {
|
|
39
|
+
let out = text.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]');
|
|
40
|
+
for (const field of ['access_token', 'refresh_token', 'id_token', 'authorization']) {
|
|
41
|
+
out = redactFormValue(out, field);
|
|
42
|
+
out = redactJsonValue(out, field);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function redactFormValue(text, field) {
|
|
47
|
+
// `field=<value>` up to a `&` (form body) with optional URL encoding.
|
|
48
|
+
return text.replace(new RegExp(`${field}=[^&\\s"']*`, 'gi'), `${field}=[REDACTED]`);
|
|
49
|
+
}
|
|
50
|
+
function redactJsonValue(text, field) {
|
|
51
|
+
return text.replace(new RegExp(`"${field}"\\s*:\\s*"[^"]*"`, 'gi'), `"${field}":"[REDACTED]"`);
|
|
52
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CodexError, redact } from './errors.js';
|
|
3
|
+
const CatalogSchema = z.object({
|
|
4
|
+
models: z.array(z.object({
|
|
5
|
+
slug: z.string(),
|
|
6
|
+
display_name: z.string(),
|
|
7
|
+
description: z.string().nullish(),
|
|
8
|
+
visibility: z.string().nullish(),
|
|
9
|
+
context_window: z.number().nullish(),
|
|
10
|
+
supported_reasoning_levels: z
|
|
11
|
+
.array(z.object({
|
|
12
|
+
effort: z.string(),
|
|
13
|
+
description: z.string().nullish(),
|
|
14
|
+
}))
|
|
15
|
+
.nullish(),
|
|
16
|
+
default_reasoning_level: z.string().nullish(),
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
export function parseModelCatalog(body) {
|
|
20
|
+
const parsed = CatalogSchema.parse(body);
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const model of parsed.models) {
|
|
23
|
+
// `visibility: "hide"` marks internal plumbing (observed: `gpt-reserve`,
|
|
24
|
+
// `codex-auto-review`); only listed models are pickable.
|
|
25
|
+
if (model.visibility === 'hide')
|
|
26
|
+
continue;
|
|
27
|
+
out.push({
|
|
28
|
+
slug: model.slug,
|
|
29
|
+
displayName: model.display_name,
|
|
30
|
+
...(model.description ? { description: model.description } : {}),
|
|
31
|
+
...(model.context_window != null ? { contextWindow: model.context_window } : {}),
|
|
32
|
+
reasoningEfforts: (model.supported_reasoning_levels ?? []).map((level) => ({
|
|
33
|
+
effort: level.effort,
|
|
34
|
+
...(level.description ? { description: level.description } : {}),
|
|
35
|
+
})),
|
|
36
|
+
...(model.default_reasoning_level
|
|
37
|
+
? { defaultReasoningEffort: model.default_reasoning_level }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Fetch the catalog, retrying once on 401 through a forced token refresh. */
|
|
44
|
+
export async function fetchModelCatalog(deps) {
|
|
45
|
+
const base = deps.protocol.responsesUrl.replace(/\/responses\/?$/, '');
|
|
46
|
+
const url = `${base}/models?client_version=${encodeURIComponent(deps.protocol.clientVersion)}`;
|
|
47
|
+
const issue = async (forceRefresh) => {
|
|
48
|
+
const token = forceRefresh ? await deps.forceRefresh() : await deps.getToken();
|
|
49
|
+
let res;
|
|
50
|
+
try {
|
|
51
|
+
res = await deps.fetchImpl(url, {
|
|
52
|
+
headers: {
|
|
53
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
54
|
+
...(token.accountId ? { 'chatgpt-account-id': token.accountId } : {}),
|
|
55
|
+
'openai-beta': 'responses=experimental',
|
|
56
|
+
originator: 'codex_cli_rs',
|
|
57
|
+
'user-agent': `codex_cli_rs/${deps.protocol.clientVersion}`,
|
|
58
|
+
accept: 'application/json',
|
|
59
|
+
},
|
|
60
|
+
signal: AbortSignal.timeout(15_000),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new CodexError('transport', redact(err instanceof Error ? err.message : String(err)));
|
|
65
|
+
}
|
|
66
|
+
return res;
|
|
67
|
+
};
|
|
68
|
+
let res = await issue(false);
|
|
69
|
+
if (res.status === 401) {
|
|
70
|
+
res = await issue(true);
|
|
71
|
+
if (res.status === 401) {
|
|
72
|
+
throw new CodexError('auth_expired', 'the catalog request was rejected after a token refresh');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (res.status === 429)
|
|
76
|
+
throw new CodexError('quota');
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
let text = '';
|
|
79
|
+
try {
|
|
80
|
+
text = redact(await res.text()).slice(0, 256);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// no body
|
|
84
|
+
}
|
|
85
|
+
throw new CodexError(res.status >= 500 ? 'transport' : 'endpoint_changed', `HTTP ${res.status}${text ? `: ${text}` : ''}`);
|
|
86
|
+
}
|
|
87
|
+
const body = await res.json().catch(() => undefined);
|
|
88
|
+
if (body === undefined)
|
|
89
|
+
throw new CodexError('transport', 'the catalog endpoint returned a non-JSON body');
|
|
90
|
+
try {
|
|
91
|
+
return parseModelCatalog(body);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// The backend changed its shape: say so instead of losing the detail.
|
|
95
|
+
throw new CodexError('endpoint_changed', `unexpected catalog shape (${err instanceof Error ? err.message.slice(0, 160) : String(err)})`);
|
|
96
|
+
}
|
|
97
|
+
}
|