@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { CodexError, redact } from './errors.js';
|
|
4
|
+
import { authorizeUrl, redirectUri } from './protocol.js';
|
|
5
|
+
export const challengeFor = (verifier) => createHash('sha256').update(verifier, 'utf8').digest('base64url');
|
|
6
|
+
export async function generatePkce() {
|
|
7
|
+
const verifier = randomBytes(64).toString('base64url');
|
|
8
|
+
return { verifier, challenge: challengeFor(verifier) };
|
|
9
|
+
}
|
|
10
|
+
export const generateState = () => randomBytes(32).toString('base64url');
|
|
11
|
+
/** Unverified JWT payload decode — metadata only, never an auth check. */
|
|
12
|
+
export function decodeJwtPayload(token) {
|
|
13
|
+
const parts = token.split('.');
|
|
14
|
+
const payload = parts.length === 3 ? parts[1] : undefined;
|
|
15
|
+
if (payload === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function authClaims(payload) {
|
|
25
|
+
const claims = payload?.['https://api.openai.com/auth'];
|
|
26
|
+
return typeof claims === 'object' && claims !== null ? claims : {};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Account id and plan from `id_token`, falling back to the access token.
|
|
30
|
+
* Values are unverified metadata: they route requests and label the UI, and
|
|
31
|
+
* nothing authorizes on them.
|
|
32
|
+
*/
|
|
33
|
+
export function extractMetadata(tokens) {
|
|
34
|
+
for (const token of [tokens.id_token, tokens.access_token]) {
|
|
35
|
+
if (!token)
|
|
36
|
+
continue;
|
|
37
|
+
const payload = decodeJwtPayload(token);
|
|
38
|
+
const claims = authClaims(payload);
|
|
39
|
+
const accountId = claims.chatgpt_account_id ??
|
|
40
|
+
(typeof payload?.chatgpt_account_id === 'string' ? payload.chatgpt_account_id : undefined);
|
|
41
|
+
if (accountId) {
|
|
42
|
+
return {
|
|
43
|
+
accountId,
|
|
44
|
+
...(claims.chatgpt_plan_type ? { planType: claims.chatgpt_plan_type } : {}),
|
|
45
|
+
...(typeof payload?.email === 'string' ? { email: payload.email } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
export async function exchangeCode(deps, code, verifier) {
|
|
52
|
+
const body = new URLSearchParams({
|
|
53
|
+
grant_type: 'authorization_code',
|
|
54
|
+
client_id: deps.protocol.clientId,
|
|
55
|
+
code,
|
|
56
|
+
code_verifier: verifier,
|
|
57
|
+
redirect_uri: redirectUri(deps.protocol),
|
|
58
|
+
});
|
|
59
|
+
return tokenRequest(deps, body);
|
|
60
|
+
}
|
|
61
|
+
export async function refreshAccessToken(deps, refreshToken) {
|
|
62
|
+
const body = new URLSearchParams({
|
|
63
|
+
grant_type: 'refresh_token',
|
|
64
|
+
client_id: deps.protocol.clientId,
|
|
65
|
+
refresh_token: refreshToken,
|
|
66
|
+
scope: deps.protocol.scope,
|
|
67
|
+
});
|
|
68
|
+
return tokenRequest(deps, body);
|
|
69
|
+
}
|
|
70
|
+
async function tokenRequest(deps, body) {
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await deps.fetchImpl(`${deps.protocol.issuer}/oauth/token`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
76
|
+
body: body.toString(),
|
|
77
|
+
signal: AbortSignal.timeout(10_000),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Timeouts and DNS failures are retryable — never an auth-expiry signal.
|
|
82
|
+
throw new CodexError('transport', redact(err instanceof Error ? err.message : String(err)));
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const detail = await errorDetail(res, deps.protocol);
|
|
86
|
+
// OAuth error codes and bare 401/403 mean this credential generation is dead.
|
|
87
|
+
if (res.status === 401 ||
|
|
88
|
+
res.status === 403 ||
|
|
89
|
+
detail.oauthError === 'invalid_grant' ||
|
|
90
|
+
detail.oauthError === 'invalid_token' ||
|
|
91
|
+
detail.oauthError === 'invalid_request') {
|
|
92
|
+
throw new CodexError('auth_expired', detail.oauthError ?? `HTTP ${res.status}`);
|
|
93
|
+
}
|
|
94
|
+
throw new CodexError('transport', `HTTP ${res.status}${detail.snippet}`);
|
|
95
|
+
}
|
|
96
|
+
const parsed = (await res.json().catch(() => undefined));
|
|
97
|
+
if (!parsed?.access_token || typeof parsed.expires_in !== 'number') {
|
|
98
|
+
throw new CodexError('transport', 'token endpoint returned an unexpected body');
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
/** Bounded, redacted body preview plus a parsed OAuth `error` code when present. */
|
|
103
|
+
async function errorDetail(res, _protocol) {
|
|
104
|
+
let text = '';
|
|
105
|
+
try {
|
|
106
|
+
text = await res.text();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
let oauthError;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(text);
|
|
114
|
+
if (typeof parsed.error === 'string')
|
|
115
|
+
oauthError = parsed.error;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// not JSON — the snippet below is still useful
|
|
119
|
+
}
|
|
120
|
+
// Redact first, truncate second: a token spanning the cut must not leak.
|
|
121
|
+
const snippet = text ? `: ${redact(text).slice(0, 512)}` : '';
|
|
122
|
+
return { ...(oauthError ? { oauthError } : {}), ...(snippet ? { snippet } : {}) };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Start one authorization flow: bind 127.0.0.1:<redirectPort>, build the
|
|
126
|
+
* authorize URL, and wait for exactly one callback. `openBrowser` is invoked
|
|
127
|
+
* once the listener is accepting (errors there are not fatal — the URL is
|
|
128
|
+
* shown in the UI too).
|
|
129
|
+
*/
|
|
130
|
+
export async function startBrowserFlow(deps) {
|
|
131
|
+
const { protocol, fetchImpl } = deps;
|
|
132
|
+
const { verifier, challenge } = await generatePkce();
|
|
133
|
+
const state = generateState();
|
|
134
|
+
const authUrl = authorizeUrl(protocol, challenge, state);
|
|
135
|
+
let resolveCompletion;
|
|
136
|
+
let rejectCompletion;
|
|
137
|
+
const completion = new Promise((resolve, reject) => {
|
|
138
|
+
resolveCompletion = resolve;
|
|
139
|
+
rejectCompletion = reject;
|
|
140
|
+
});
|
|
141
|
+
let server;
|
|
142
|
+
try {
|
|
143
|
+
server = createServer((req, res) => {
|
|
144
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${protocol.redirectPort}`);
|
|
145
|
+
if (url.pathname === protocol.redirectPath) {
|
|
146
|
+
finish(url, res);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
150
|
+
res.end('Not found');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
throw new CodexError('transport', `could not start the sign-in callback server (${err instanceof Error ? err.message : String(err)})`);
|
|
155
|
+
}
|
|
156
|
+
const shutdown = () => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
server.close();
|
|
159
|
+
};
|
|
160
|
+
const rejectPending = (err) => {
|
|
161
|
+
shutdown();
|
|
162
|
+
rejectCompletion?.(err);
|
|
163
|
+
};
|
|
164
|
+
const finish = (url, res) => {
|
|
165
|
+
const error = url.searchParams.get('error');
|
|
166
|
+
const code = url.searchParams.get('code');
|
|
167
|
+
const returnedState = url.searchParams.get('state') ?? '';
|
|
168
|
+
let failure;
|
|
169
|
+
if (error) {
|
|
170
|
+
failure = new CodexError('transport', `authorization was refused: ${error}`);
|
|
171
|
+
}
|
|
172
|
+
else if (returnedState.length !== state.length || returnedState !== state) {
|
|
173
|
+
// Byte-compare over the full length; a mismatch is a CSRF attempt or a stale tab.
|
|
174
|
+
failure = new CodexError('state_mismatch');
|
|
175
|
+
}
|
|
176
|
+
else if (!code) {
|
|
177
|
+
failure = new CodexError('transport', 'the sign-in callback carried no authorization code');
|
|
178
|
+
}
|
|
179
|
+
shutdown();
|
|
180
|
+
if (failure || !code) {
|
|
181
|
+
respondHtml(res, false);
|
|
182
|
+
rejectCompletion?.(failure ?? new CodexError('transport', 'the callback carried no code'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
respondHtml(res, true);
|
|
186
|
+
exchangeCode({ fetchImpl, protocol }, code, verifier).then(resolveCompletion, (err) => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
rejectCompletion?.(err);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
const timer = setTimeout(() => rejectPending(new CodexError('transport', 'the sign-in window timed out')), protocol.authTimeoutMs);
|
|
192
|
+
await new Promise((resolve, reject) => {
|
|
193
|
+
server.once('error', (err) => {
|
|
194
|
+
reject(err.code === 'EADDRINUSE'
|
|
195
|
+
? new CodexError('transport', `port ${protocol.redirectPort} is already in use — close whatever holds it` +
|
|
196
|
+
' (often the Codex CLI) and retry')
|
|
197
|
+
: new CodexError('transport', err.message));
|
|
198
|
+
});
|
|
199
|
+
server.listen(protocol.redirectPort, '127.0.0.1', resolve);
|
|
200
|
+
});
|
|
201
|
+
deps.openBrowser?.(authUrl);
|
|
202
|
+
return {
|
|
203
|
+
authUrl,
|
|
204
|
+
completion,
|
|
205
|
+
cancel: (reason) => rejectPending(new CodexError('transport', reason)),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Small self-contained page; callback values are never interpolated into it. */
|
|
209
|
+
function respondHtml(res, ok) {
|
|
210
|
+
const body = `<!doctype html><meta charset="utf-8"><title>coja</title>
|
|
211
|
+
<body style="font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
212
|
+
<p style="max-width:28rem;text-align:center;line-height:1.5">${ok ? 'ChatGPT connected. You can close this tab and return to coja.' : 'The ChatGPT sign-in did not complete. Return to coja and try again.'}</p>`;
|
|
213
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
214
|
+
res.end(body);
|
|
215
|
+
}
|
|
216
|
+
/** One shared session id per provider instance, as the backend expects. */
|
|
217
|
+
export const newSessionId = () => randomUUID();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire constants for the "Sign in with ChatGPT" provider
|
|
3
|
+
* (docs/research/chatgpt-subscription-auth.md). These ride OpenAI's public
|
|
4
|
+
* Codex CLI OAuth client against the undocumented ChatGPT Codex backend;
|
|
5
|
+
* every value is overridable so tests can point at a local mock.
|
|
6
|
+
*/
|
|
7
|
+
export const CODEX_PROTOCOL = {
|
|
8
|
+
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
9
|
+
issuer: 'https://auth.openai.com',
|
|
10
|
+
redirectPort: 1455,
|
|
11
|
+
redirectPath: '/auth/callback',
|
|
12
|
+
scope: 'openid profile email offline_access',
|
|
13
|
+
responsesUrl: 'https://chatgpt.com/backend-api/codex/responses',
|
|
14
|
+
clientVersion: '0.144.6',
|
|
15
|
+
refreshMarginMs: 120_000,
|
|
16
|
+
authTimeoutMs: 5 * 60_000,
|
|
17
|
+
};
|
|
18
|
+
/** Parameters the Codex backend answers with `400 Unsupported parameter` (naming only the first). */
|
|
19
|
+
export const UNSUPPORTED_BODY_PARAMETERS = [
|
|
20
|
+
'frequency_penalty',
|
|
21
|
+
'logit_bias',
|
|
22
|
+
'max_output_tokens',
|
|
23
|
+
'max_tool_calls',
|
|
24
|
+
'metadata',
|
|
25
|
+
'presence_penalty',
|
|
26
|
+
'safety_identifier',
|
|
27
|
+
'seed',
|
|
28
|
+
'stream_options',
|
|
29
|
+
'temperature',
|
|
30
|
+
'top_logprobs',
|
|
31
|
+
'top_p',
|
|
32
|
+
'truncation',
|
|
33
|
+
'user',
|
|
34
|
+
];
|
|
35
|
+
export function authorizeUrl(protocol, challenge, state) {
|
|
36
|
+
const params = new URLSearchParams({
|
|
37
|
+
response_type: 'code',
|
|
38
|
+
client_id: protocol.clientId,
|
|
39
|
+
redirect_uri: redirectUri(protocol),
|
|
40
|
+
scope: protocol.scope,
|
|
41
|
+
code_challenge: challenge,
|
|
42
|
+
code_challenge_method: 'S256',
|
|
43
|
+
state,
|
|
44
|
+
id_token_add_organizations: 'true',
|
|
45
|
+
codex_cli_simplified_flow: 'true',
|
|
46
|
+
});
|
|
47
|
+
return `${protocol.issuer}/oauth/authorize?${params.toString()}`;
|
|
48
|
+
}
|
|
49
|
+
export const redirectUri = (protocol) => `http://localhost:${protocol.redirectPort}${protocol.redirectPath}`;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { CodexError, redact } from './errors.js';
|
|
3
|
+
import { newSessionId } from './oauth.js';
|
|
4
|
+
import { CODEX_PROTOCOL, UNSUPPORTED_BODY_PARAMETERS } from './protocol.js';
|
|
5
|
+
export function createCodexLanguageModel(deps) {
|
|
6
|
+
const protocol = deps.protocol ?? CODEX_PROTOCOL;
|
|
7
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
8
|
+
const sessionId = newSessionId();
|
|
9
|
+
// createOpenAI appends `/responses` to the base URL.
|
|
10
|
+
const baseURL = protocol.responsesUrl.replace(/\/responses\/?$/, '');
|
|
11
|
+
const request = async (input, init) => {
|
|
12
|
+
const rawBody = typeof init?.body === 'string' ? init.body : undefined;
|
|
13
|
+
const parsedBody = rawBody ? JSON.parse(rawBody) : undefined;
|
|
14
|
+
const streaming = parsedBody?.stream === true;
|
|
15
|
+
const url = new URL(typeof input === 'string' ? input : input.toString());
|
|
16
|
+
url.searchParams.set('client_version', protocol.clientVersion);
|
|
17
|
+
const issue = async (forceRefresh) => {
|
|
18
|
+
const token = forceRefresh
|
|
19
|
+
? await deps.tokens.forceRefresh()
|
|
20
|
+
: await deps.tokens.getFreshAccessToken();
|
|
21
|
+
const effort = deps.reasoningEffort ?? deps.defaultReasoningEffort;
|
|
22
|
+
try {
|
|
23
|
+
return await fetchImpl(url, {
|
|
24
|
+
...init,
|
|
25
|
+
...(parsedBody ? { body: JSON.stringify(subscriptionBody(parsedBody, effort)) } : {}),
|
|
26
|
+
headers: {
|
|
27
|
+
...Object.fromEntries(new Headers(init?.headers)),
|
|
28
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
29
|
+
...(token.accountId ? { 'chatgpt-account-id': token.accountId } : {}),
|
|
30
|
+
'openai-beta': 'responses=experimental',
|
|
31
|
+
originator: 'codex_cli_rs',
|
|
32
|
+
session_id: sessionId,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// DNS/TLS/reset and our own aborts surface as a typed, redacted error
|
|
38
|
+
// instead of a raw TypeError no one can classify.
|
|
39
|
+
throw new CodexError('transport', redact(err instanceof Error ? err.message : String(err)));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
let response = await issue(false);
|
|
43
|
+
if (response.status === 401) {
|
|
44
|
+
response = await issue(true);
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
throw new CodexError('auth_expired', 'the request was rejected after a token refresh');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (response.status === 429) {
|
|
50
|
+
const retryAfter = Number(response.headers.get('retry-after'));
|
|
51
|
+
throw new CodexError('quota', undefined, {
|
|
52
|
+
...(Number.isFinite(retryAfter) && retryAfter > 0
|
|
53
|
+
? { retryAfterMs: retryAfter * 1000 }
|
|
54
|
+
: {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new CodexError(endpointErrorCode(response.status), await bodySnippet(response, protocol));
|
|
59
|
+
}
|
|
60
|
+
if (parsedBody && !streaming)
|
|
61
|
+
return collapseStreamedResponse(response);
|
|
62
|
+
return response;
|
|
63
|
+
};
|
|
64
|
+
const openai = createOpenAI({
|
|
65
|
+
// Unused: the wrapper replaces the Authorization header on every request.
|
|
66
|
+
apiKey: 'coja-chatgpt-subscription',
|
|
67
|
+
baseURL,
|
|
68
|
+
fetch: request,
|
|
69
|
+
});
|
|
70
|
+
return openai.responses(deps.modelId);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rewrite the SDK's request body into what the subscription backend accepts.
|
|
74
|
+
* The backend rejects the whole request when any of the unsupported
|
|
75
|
+
* parameters is present and names only the first offender, so the list is
|
|
76
|
+
* enforced here rather than at every call site.
|
|
77
|
+
*/
|
|
78
|
+
export function subscriptionBody(body, reasoningEffort) {
|
|
79
|
+
const rewritten = { ...body };
|
|
80
|
+
for (const parameter of UNSUPPORTED_BODY_PARAMETERS)
|
|
81
|
+
delete rewritten[parameter];
|
|
82
|
+
rewritten.stream = true;
|
|
83
|
+
rewritten.store = false;
|
|
84
|
+
rewritten.parallel_tool_calls = false;
|
|
85
|
+
if (Array.isArray(rewritten.input)) {
|
|
86
|
+
rewritten.input = stripServerSideState(rewritten.input);
|
|
87
|
+
}
|
|
88
|
+
// The catalog publishes per-model reasoning efforts (including levels the
|
|
89
|
+
// AI SDK's own provider-option schema does not know, e.g. `ultra`), so the
|
|
90
|
+
// effort is injected here rather than through provider options. An explicit
|
|
91
|
+
// `reasoning` on the request always wins.
|
|
92
|
+
if (reasoningEffort && rewritten.reasoning === undefined) {
|
|
93
|
+
rewritten.reasoning = { effort: reasoningEffort };
|
|
94
|
+
}
|
|
95
|
+
// `store:false` turns: reasoning items must come back encrypted, or a tool
|
|
96
|
+
// call can lose its required preceding reasoning item.
|
|
97
|
+
const include = Array.isArray(rewritten.include) ? [...rewritten.include] : [];
|
|
98
|
+
if (!include.includes('reasoning.encrypted_content'))
|
|
99
|
+
include.push('reasoning.encrypted_content');
|
|
100
|
+
rewritten.include = include;
|
|
101
|
+
return rewritten;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* With `store:false` the backend persists nothing, so input items that carry
|
|
105
|
+
* server-side ids from a previous response (`msg_…`, `fc_…`, `rs_…`) reference
|
|
106
|
+
* records that do not exist and the whole request is refused
|
|
107
|
+
* (`Item with id '…' not found`). `call_id` stays: it is the local key pairing
|
|
108
|
+
* a `function_call` with its `function_call_output`. `item_reference` entries
|
|
109
|
+
* only ever point at stored state, so they are dropped outright.
|
|
110
|
+
*/
|
|
111
|
+
export function stripServerSideState(items) {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
115
|
+
out.push(item);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const record = item;
|
|
119
|
+
if (record.type === 'item_reference')
|
|
120
|
+
continue;
|
|
121
|
+
const clone = { ...record };
|
|
122
|
+
delete clone.id;
|
|
123
|
+
out.push(clone);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `doGenerate` issued a non-streaming request; the backend only speaks
|
|
129
|
+
* streaming, so the SSE has to collapse into one JSON response. This
|
|
130
|
+
* backend's `response.completed` carries `output: []` — the turn's items only
|
|
131
|
+
* ever arrive via `response.output_item.done`, so a completed payload with an
|
|
132
|
+
* empty output gets the collected items restored.
|
|
133
|
+
*/
|
|
134
|
+
async function collapseStreamedResponse(response) {
|
|
135
|
+
if (!response.body)
|
|
136
|
+
throw new CodexError('transport', 'the backend returned no response stream');
|
|
137
|
+
const events = parseSse(await response.text());
|
|
138
|
+
let completed;
|
|
139
|
+
let failure;
|
|
140
|
+
const items = [];
|
|
141
|
+
for (const event of events) {
|
|
142
|
+
if (event.name === 'response.completed')
|
|
143
|
+
completed = event.data;
|
|
144
|
+
else if (event.name === 'response.failed' || event.name === 'error')
|
|
145
|
+
failure = event.data;
|
|
146
|
+
else if (event.name === 'response.output_item.done') {
|
|
147
|
+
const item = event.data?.item;
|
|
148
|
+
if (item !== undefined)
|
|
149
|
+
items.push(item);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!completed) {
|
|
153
|
+
const detail = failure ? `: ${JSON.stringify(failure).slice(0, 512)}` : '';
|
|
154
|
+
throw new CodexError('transport', `the response stream ended without completing${detail}`);
|
|
155
|
+
}
|
|
156
|
+
const payload = (completed.response ?? completed);
|
|
157
|
+
const restored = Array.isArray(payload.output) && payload.output.length > 0
|
|
158
|
+
? payload
|
|
159
|
+
: { ...payload, output: items };
|
|
160
|
+
return new Response(JSON.stringify(restored), {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { 'content-type': 'application/json' },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/** Minimal SSE reader for the collapse path (enough for this backend's frames). */
|
|
166
|
+
export function parseSse(text) {
|
|
167
|
+
const events = [];
|
|
168
|
+
for (const block of text.split(/\r?\n\r?\n|\r\n\r\n/)) {
|
|
169
|
+
let name = 'message';
|
|
170
|
+
const dataLines = [];
|
|
171
|
+
for (const line of block.split(/\r?\n|\r\n/)) {
|
|
172
|
+
if (line.startsWith(':'))
|
|
173
|
+
continue;
|
|
174
|
+
if (line.startsWith('event:'))
|
|
175
|
+
name = line.slice(6).trim();
|
|
176
|
+
else if (line.startsWith('data:'))
|
|
177
|
+
dataLines.push(line.slice(5).trimStart());
|
|
178
|
+
}
|
|
179
|
+
const data = dataLines.join('\n');
|
|
180
|
+
if (data === '' || data === '[DONE]')
|
|
181
|
+
continue;
|
|
182
|
+
try {
|
|
183
|
+
const parsed = JSON.parse(data);
|
|
184
|
+
events.push({
|
|
185
|
+
name: typeof parsed.type === 'string' ? parsed.type : name,
|
|
186
|
+
data: parsed,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
events.push({ name, data: undefined });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return events;
|
|
194
|
+
}
|
|
195
|
+
/** 4xx on this backend almost always means the request shape drifted or a plan gate. */
|
|
196
|
+
function endpointErrorCode(status) {
|
|
197
|
+
return status >= 500 ? 'transport' : 'endpoint_changed';
|
|
198
|
+
}
|
|
199
|
+
async function bodySnippet(response, _protocol) {
|
|
200
|
+
let text = '';
|
|
201
|
+
try {
|
|
202
|
+
text = await response.text();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return `HTTP ${response.status}`;
|
|
206
|
+
}
|
|
207
|
+
// Redact first, truncate second: a token spanning the cut must not leak.
|
|
208
|
+
return `HTTP ${response.status}${text ? `: ${redact(text).slice(0, 512)}` : ''}`;
|
|
209
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, resolveDataDir } from './data-dir.js';
|
|
3
|
+
import { openDb } from './db.js';
|
|
4
|
+
export function createContext(opts = {}) {
|
|
5
|
+
const dataDir = ensureDir(opts.dataDir ?? resolveDataDir());
|
|
6
|
+
const db = openDb(path.join(dataDir, 'coja.db'));
|
|
7
|
+
return { dataDir, db };
|
|
8
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { settings } from './db.js';
|
|
3
|
+
/**
|
|
4
|
+
* User-added model providers (Setup → "Add model provider"): any endpoint
|
|
5
|
+
* speaking the OpenAI chat-completions or Anthropic messages protocol at a
|
|
6
|
+
* custom base URL. Configs live in the settings table as one JSON document —
|
|
7
|
+
* a handful of entries at most, so a document beats a table — while API keys
|
|
8
|
+
* go to the SecretStore under the provider id, exactly like built-in keys.
|
|
9
|
+
*/
|
|
10
|
+
const SETTINGS_KEY = 'custom_providers';
|
|
11
|
+
export const CUSTOM_PROVIDER_ID_PREFIX = 'custom-';
|
|
12
|
+
/**
|
|
13
|
+
* Models arrive as ids, with an optional display name once one is known
|
|
14
|
+
* (fetched from the endpoint's model list or edited in). Stored configs from
|
|
15
|
+
* before labels existed hold plain strings — normalized on read.
|
|
16
|
+
*/
|
|
17
|
+
const modelEntrySchema = z.union([
|
|
18
|
+
z
|
|
19
|
+
.string()
|
|
20
|
+
.transform((id) => ({ id: id.trim() }))
|
|
21
|
+
.pipe(z.object({ id: z.string().min(1).max(200) })),
|
|
22
|
+
z.object({
|
|
23
|
+
id: z.string().trim().min(1).max(200),
|
|
24
|
+
label: z.string().trim().min(1).max(200).optional(),
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
const normalizeModels = (models) => {
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const model of models) {
|
|
31
|
+
if (!model.id || seen.has(model.id))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(model.id);
|
|
34
|
+
out.push(model.label ? { id: model.id, label: model.label } : { id: model.id });
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
const modelsSchema = z
|
|
39
|
+
.array(modelEntrySchema)
|
|
40
|
+
.min(1, 'Add at least one model')
|
|
41
|
+
.max(32)
|
|
42
|
+
.transform(normalizeModels);
|
|
43
|
+
const configSchema = z.object({
|
|
44
|
+
id: z.string().regex(/^custom-[a-z0-9][a-z0-9-]*$/),
|
|
45
|
+
name: z.string().min(1).max(64),
|
|
46
|
+
baseUrl: z.string().url(),
|
|
47
|
+
apiFormat: z.enum(['openai', 'anthropic']),
|
|
48
|
+
models: modelsSchema,
|
|
49
|
+
});
|
|
50
|
+
export const addCustomProviderSchema = z.object({
|
|
51
|
+
name: z.string().trim().min(1, 'Name is required').max(64),
|
|
52
|
+
baseUrl: z
|
|
53
|
+
.string()
|
|
54
|
+
.trim()
|
|
55
|
+
.min(1, 'Base URL is required')
|
|
56
|
+
.max(2000)
|
|
57
|
+
.refine((value) => {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(value);
|
|
60
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}, 'Base URL must be an http(s) URL'),
|
|
66
|
+
apiFormat: z.enum(['openai', 'anthropic']),
|
|
67
|
+
models: modelsSchema,
|
|
68
|
+
apiKey: z.string().max(4096).optional(),
|
|
69
|
+
});
|
|
70
|
+
/** `deepseek` / `My Proxy 2` → `deepseek` / `my-proxy-2` (the id is `custom-<slug>`). */
|
|
71
|
+
export function slugifyProviderName(name) {
|
|
72
|
+
const slug = name
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
75
|
+
.replace(/^-+|-+$/g, '')
|
|
76
|
+
.slice(0, 40)
|
|
77
|
+
.replace(/-+$/g, '');
|
|
78
|
+
return slug || 'provider';
|
|
79
|
+
}
|
|
80
|
+
function readAll(db) {
|
|
81
|
+
const raw = settings.get(db, SETTINGS_KEY);
|
|
82
|
+
if (!raw)
|
|
83
|
+
return [];
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
if (!Array.isArray(parsed))
|
|
87
|
+
return [];
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const entry of parsed) {
|
|
90
|
+
const result = configSchema.safeParse(entry);
|
|
91
|
+
if (result.success)
|
|
92
|
+
out.push(result.data);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function writeAll(db, providers) {
|
|
101
|
+
settings.set(db, SETTINGS_KEY, JSON.stringify(providers));
|
|
102
|
+
}
|
|
103
|
+
export function listCustomProviders(db) {
|
|
104
|
+
return readAll(db);
|
|
105
|
+
}
|
|
106
|
+
export function getCustomProvider(db, id) {
|
|
107
|
+
return readAll(db).find((provider) => provider.id === id);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Adds (or replaces — same id means an intentional edit) a custom provider
|
|
111
|
+
* from a validated request. Returns the saved config (with its id) and the
|
|
112
|
+
* new full list.
|
|
113
|
+
*/
|
|
114
|
+
export function saveCustomProvider(db, request) {
|
|
115
|
+
const id = `${CUSTOM_PROVIDER_ID_PREFIX}${slugifyProviderName(request.name)}`;
|
|
116
|
+
const rest = readAll(db).filter((provider) => provider.id !== id);
|
|
117
|
+
const saved = {
|
|
118
|
+
id,
|
|
119
|
+
name: request.name,
|
|
120
|
+
baseUrl: request.baseUrl,
|
|
121
|
+
apiFormat: request.apiFormat,
|
|
122
|
+
models: normalizeModels(request.models.map((m) => (typeof m === 'string' ? { id: m } : m))),
|
|
123
|
+
};
|
|
124
|
+
writeAll(db, [...rest, saved]);
|
|
125
|
+
return { saved, providers: readAll(db) };
|
|
126
|
+
}
|
|
127
|
+
export function deleteCustomProvider(db, id) {
|
|
128
|
+
const providers = readAll(db);
|
|
129
|
+
const rest = providers.filter((provider) => provider.id !== id);
|
|
130
|
+
if (rest.length === providers.length)
|
|
131
|
+
return false;
|
|
132
|
+
writeAll(db, rest);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
package/dist/data-dir.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Per-user application data directory (SQLite database, app-managed bare
|
|
6
|
+
* clones). Override with `COJA_DATA_DIR` — tests and scratch runs use that.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveDataDir(env = process.env) {
|
|
9
|
+
const override = env.COJA_DATA_DIR;
|
|
10
|
+
if (override)
|
|
11
|
+
return path.resolve(override);
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
switch (process.platform) {
|
|
14
|
+
case 'darwin':
|
|
15
|
+
return path.join(home, 'Library', 'Application Support', 'coja');
|
|
16
|
+
case 'win32':
|
|
17
|
+
return path.join(env.APPDATA ?? path.join(home, 'AppData', 'Roaming'), 'coja');
|
|
18
|
+
default:
|
|
19
|
+
return path.join(env.XDG_DATA_HOME ?? path.join(home, '.local', 'share'), 'coja');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function ensureDir(dir) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|