@joeyshi12/casper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/casper.json +14 -0
- package/dist/casper.js +23 -0
- package/dist/server.js +3506 -0
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/abap-DsBKuouk.js +1 -0
- package/dist/web/assets/abnfDiagram-N423BO3Z-D4dAP-1-.js +1 -0
- package/dist/web/assets/actionscript-3-D_z4Izcz.js +1 -0
- package/dist/web/assets/ada-727ZlQH0.js +1 -0
- package/dist/web/assets/andromeeda-C3khCPGq.js +1 -0
- package/dist/web/assets/angular-html-LfdN0zeE.js +1 -0
- package/dist/web/assets/angular-ts-CKsD7JZE.js +1 -0
- package/dist/web/assets/apache-Dn00JSTd.js +1 -0
- package/dist/web/assets/apex-COJ4H7py.js +1 -0
- package/dist/web/assets/apl-BBq3IX1j.js +1 -0
- package/dist/web/assets/applescript-Bu5BbsvL.js +1 -0
- package/dist/web/assets/ara-7O62HKoU.js +1 -0
- package/dist/web/assets/arc-CicRYRW0.js +1 -0
- package/dist/web/assets/architectureDiagram-T3A2C74G-C8_jIuYI.js +36 -0
- package/dist/web/assets/asciidoc-BPT9niGB.js +1 -0
- package/dist/web/assets/asm-Dhn9LcZ4.js +1 -0
- package/dist/web/assets/astro-CqkE3fuf.js +1 -0
- package/dist/web/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/web/assets/awk-eg146-Ew.js +1 -0
- package/dist/web/assets/ayu-dark-Cv9koXgw.js +1 -0
- package/dist/web/assets/ballerina-Du268qiB.js +1 -0
- package/dist/web/assets/bat-fje9CFhw.js +1 -0
- package/dist/web/assets/beancount-BwXTMy5W.js +1 -0
- package/dist/web/assets/berry-3xVqZejG.js +1 -0
- package/dist/web/assets/bibtex-xW4inM5L.js +1 -0
- package/dist/web/assets/bicep-DHo0CJ0O.js +1 -0
- package/dist/web/assets/blade-a8OxSdnT.js +1 -0
- package/dist/web/assets/blockDiagram-VBNYF7ZC-1z_qaXwF.js +132 -0
- package/dist/web/assets/bsl-Dgyn0ogV.js +1 -0
- package/dist/web/assets/c-C3t2pwGQ.js +1 -0
- package/dist/web/assets/c4Diagram-5PPSVZJV-DA2HxqMu.js +10 -0
- package/dist/web/assets/cadence-DNquZEk8.js +1 -0
- package/dist/web/assets/cairo--RitsXJZ.js +1 -0
- package/dist/web/assets/catppuccin-frappe-CD_QflpE.js +1 -0
- package/dist/web/assets/catppuccin-latte-DRW-0cLl.js +1 -0
- package/dist/web/assets/catppuccin-macchiato-C-_shW-Y.js +1 -0
- package/dist/web/assets/catppuccin-mocha-LGGdnPYs.js +1 -0
- package/dist/web/assets/channel-DdA8Dcii.js +1 -0
- package/dist/web/assets/chunk-2GRJ4B5K-R9Gd-FO9.js +1 -0
- package/dist/web/assets/chunk-2Q5K7J3B-DBVFWztp.js +1 -0
- package/dist/web/assets/chunk-5RXB4S5H-bLTobZeO.js +231 -0
- package/dist/web/assets/chunk-5VM5RSS4-Bn8X-QeG.js +15 -0
- package/dist/web/assets/chunk-6Q2QTUOP-CQtChnAA.js +88 -0
- package/dist/web/assets/chunk-GF5L2VYU-DUrrlayx.js +206 -0
- package/dist/web/assets/chunk-JWPE2WC7-BgI2ovD2.js +1 -0
- package/dist/web/assets/chunk-KBJHAD2P-UMKTTxF1.js +1 -0
- package/dist/web/assets/chunk-RYQCIY6F-BOcO7eGL.js +1 -0
- package/dist/web/assets/chunk-XXDRQBXY-DE8-7iM5.js +1 -0
- package/dist/web/assets/clarity-BHOwM8T6.js +1 -0
- package/dist/web/assets/classDiagram-JCYQIIEL-Dxx1nEKY.js +1 -0
- package/dist/web/assets/classDiagram-v2-OCEON4UE-Dxx1nEKY.js +1 -0
- package/dist/web/assets/clojure-DxSadP1t.js +1 -0
- package/dist/web/assets/cmake-DbXoA79R.js +1 -0
- package/dist/web/assets/cobol-PTqiYgYu.js +1 -0
- package/dist/web/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/web/assets/codeql-sacFqUAJ.js +1 -0
- package/dist/web/assets/coffee-dyiR41kL.js +1 -0
- package/dist/web/assets/common-lisp-C7gG9l05.js +1 -0
- package/dist/web/assets/coq-Dsg_Bt_b.js +1 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-BAygCVU2.js +1 -0
- package/dist/web/assets/cpp-BksuvNSY.js +1 -0
- package/dist/web/assets/crystal-DtDmRg-F.js +1 -0
- package/dist/web/assets/csharp-D9R-vmeu.js +1 -0
- package/dist/web/assets/css-BPhBrDlE.js +1 -0
- package/dist/web/assets/csv-B0qRVHPH.js +1 -0
- package/dist/web/assets/cue-DtFQj3wx.js +1 -0
- package/dist/web/assets/cynefin-VYW2F7L2-CSJk_SPs.js +178 -0
- package/dist/web/assets/cynefinDiagram-MW4NZA55-C0yeigu-.js +62 -0
- package/dist/web/assets/cypher-m2LEI-9-.js +1 -0
- package/dist/web/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/dist/web/assets/d-BoXegm-a.js +1 -0
- package/dist/web/assets/dagre-VZM6K2ZE-C7Wka8DN.js +4 -0
- package/dist/web/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/web/assets/dart-B9wLZaAG.js +1 -0
- package/dist/web/assets/dax-ClGRhx96.js +1 -0
- package/dist/web/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/web/assets/desktop-DEIpsLCJ.js +1 -0
- package/dist/web/assets/diagram-7IWD3JNH-D6dg2rEx.js +30 -0
- package/dist/web/assets/diagram-B4RE2ZJO-D4_nbLMB.js +3 -0
- package/dist/web/assets/diagram-LBJQPF4R-Nht6dUst.js +24 -0
- package/dist/web/assets/diagram-Q27KOJAE-Dr47m9ou.js +24 -0
- package/dist/web/assets/diagram-UB23O5K3-wBr0ZbcL.js +41 -0
- package/dist/web/assets/diff-BgYniUM_.js +1 -0
- package/dist/web/assets/docker-COcR7UxN.js +1 -0
- package/dist/web/assets/dotenv-BjQB5zDj.js +1 -0
- package/dist/web/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/web/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/web/assets/dream-maker-C-nORZOA.js +1 -0
- package/dist/web/assets/ebnfDiagram-BXEA7PRR-BLB0X9R-.js +1 -0
- package/dist/web/assets/edge-D5gP-w-T.js +1 -0
- package/dist/web/assets/elixir-CLiX3zqd.js +1 -0
- package/dist/web/assets/elm-CmHSxxaM.js +1 -0
- package/dist/web/assets/emacs-lisp-BX77sIaO.js +1 -0
- package/dist/web/assets/erDiagram-JOGREHBK-Dju9_T31.js +85 -0
- package/dist/web/assets/erb-BYTLMnw6.js +1 -0
- package/dist/web/assets/erlang-B-DoSBHF.js +1 -0
- package/dist/web/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/web/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/web/assets/fennel-bCA53EVm.js +1 -0
- package/dist/web/assets/fish-w-ucz2PV.js +1 -0
- package/dist/web/assets/flowDiagram-UKHOOZJN-DGKxclCZ.js +156 -0
- package/dist/web/assets/fluent-Dayu4EKP.js +1 -0
- package/dist/web/assets/fortran-fixed-form-TqA4NnZg.js +1 -0
- package/dist/web/assets/fortran-free-form-DKXYxT9g.js +1 -0
- package/dist/web/assets/fsharp-XplgxFYe.js +1 -0
- package/dist/web/assets/ganttDiagram-PKOTCBZU-CO3GCXs9.js +292 -0
- package/dist/web/assets/gdresource-BHYsBjWJ.js +1 -0
- package/dist/web/assets/gdscript-DfxzS6Rs.js +1 -0
- package/dist/web/assets/gdshader-SKMF96pI.js +1 -0
- package/dist/web/assets/genie-ajMbGru0.js +1 -0
- package/dist/web/assets/gherkin--30QC5Em.js +1 -0
- package/dist/web/assets/git-commit-i4q6IMui.js +1 -0
- package/dist/web/assets/git-rebase-B-v9cOL2.js +1 -0
- package/dist/web/assets/gitGraphDiagram-DS77QQ5N-HJ2a9e8_.js +106 -0
- package/dist/web/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/web/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/web/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/web/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/web/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/web/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/web/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/web/assets/gleam-B430Bg39.js +1 -0
- package/dist/web/assets/glimmer-js-D-cwc0-E.js +1 -0
- package/dist/web/assets/glimmer-ts-pgjy16dm.js +1 -0
- package/dist/web/assets/glsl-DBO2IWDn.js +1 -0
- package/dist/web/assets/gnuplot-CM8KxXT1.js +1 -0
- package/dist/web/assets/go-B1SYOhNW.js +1 -0
- package/dist/web/assets/graph-C9eacEi8.js +1 -0
- package/dist/web/assets/graphql-cDcHW_If.js +1 -0
- package/dist/web/assets/groovy-DkBy-JyN.js +1 -0
- package/dist/web/assets/hack-D1yCygmZ.js +1 -0
- package/dist/web/assets/haml-B2EZWmdv.js +1 -0
- package/dist/web/assets/handlebars-BQGss363.js +1 -0
- package/dist/web/assets/haskell-BILxekzW.js +1 -0
- package/dist/web/assets/haxe-C5wWYbrZ.js +1 -0
- package/dist/web/assets/hcl-HzYwdGDm.js +1 -0
- package/dist/web/assets/hjson-T-Tgc4AT.js +1 -0
- package/dist/web/assets/hlsl-ifBTmRxC.js +1 -0
- package/dist/web/assets/houston-DnULxvSX.js +1 -0
- package/dist/web/assets/html-C2L_23MC.js +1 -0
- package/dist/web/assets/html-derivative-CSfWNPLT.js +1 -0
- package/dist/web/assets/http-FRrOvY1W.js +1 -0
- package/dist/web/assets/hxml-TIA70rKU.js +1 -0
- package/dist/web/assets/hy-BMj5Y0dO.js +1 -0
- package/dist/web/assets/imba-bv_oIlVt.js +1 -0
- package/dist/web/assets/index-56vlRQsM.js +342 -0
- package/dist/web/assets/index-CZ8H0616.css +1 -0
- package/dist/web/assets/index-CguDTw5b.js +13 -0
- package/dist/web/assets/infoDiagram-6WML65LV-BaVcr3f7.js +2 -0
- package/dist/web/assets/ini-BjABl1g7.js +1 -0
- package/dist/web/assets/init-Gi6I4Gst.js +1 -0
- package/dist/web/assets/ishikawaDiagram-WSZJBQD7-CBW9kPX2.js +70 -0
- package/dist/web/assets/java-xI-RfyKK.js +1 -0
- package/dist/web/assets/javascript-ySlJ1b_l.js +1 -0
- package/dist/web/assets/jinja-DGy0s7-h.js +1 -0
- package/dist/web/assets/jison-BqZprYcd.js +1 -0
- package/dist/web/assets/journeyDiagram-NVQOT4AX-ByxvAnge.js +139 -0
- package/dist/web/assets/json-BQoSv7ci.js +1 -0
- package/dist/web/assets/json5-w8dY5SsB.js +1 -0
- package/dist/web/assets/jsonc-TU54ms6u.js +1 -0
- package/dist/web/assets/jsonl-DREVFZK8.js +1 -0
- package/dist/web/assets/jsonnet-BfivnA6A.js +1 -0
- package/dist/web/assets/jssm-P4WzXJd0.js +1 -0
- package/dist/web/assets/jsx-BAng5TT0.js +1 -0
- package/dist/web/assets/julia-BBuGR-5E.js +1 -0
- package/dist/web/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/web/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/web/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/web/assets/kanban-definition-27J2QSJJ-Cax1fcgb.js +89 -0
- package/dist/web/assets/kotlin-B5lbUyaz.js +1 -0
- package/dist/web/assets/kusto-mebxcVVE.js +1 -0
- package/dist/web/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/web/assets/latex-C-cWTeAZ.js +1 -0
- package/dist/web/assets/layout-DEXfKzaS.js +1 -0
- package/dist/web/assets/lean-XBlWyCtg.js +1 -0
- package/dist/web/assets/less-BfCpw3nA.js +1 -0
- package/dist/web/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/web/assets/linear-CBgLWOFx.js +1 -0
- package/dist/web/assets/liquid-D3W5UaiH.js +1 -0
- package/dist/web/assets/log-Cc5clBb7.js +1 -0
- package/dist/web/assets/logo-IuBKFhSY.js +1 -0
- package/dist/web/assets/lua-CvWAzNxB.js +1 -0
- package/dist/web/assets/luau-Du5NY7AG.js +1 -0
- package/dist/web/assets/make-Bvotw-X0.js +1 -0
- package/dist/web/assets/map-Czzmt4hB.js +1 -0
- package/dist/web/assets/markdown-UIAJJxZW.js +1 -0
- package/dist/web/assets/marko-z0MBrx5-.js +1 -0
- package/dist/web/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/web/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/web/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/web/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/web/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/web/assets/matlab-D9-PGadD.js +1 -0
- package/dist/web/assets/mdc-DB_EDNY_.js +1 -0
- package/dist/web/assets/mdx-sdHcTMYB.js +1 -0
- package/dist/web/assets/mermaid-Ci6OQyBP.js +1 -0
- package/dist/web/assets/mermaid.core-B3afE_Bn.js +314 -0
- package/dist/web/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/web/assets/min-light-CTRr51gU.js +1 -0
- package/dist/web/assets/mindmap-definition-FAOFIHXS-sS1s_igC.js +96 -0
- package/dist/web/assets/mipsasm-BC5c_5Pe.js +1 -0
- package/dist/web/assets/mojo-Tz6hzZYG.js +1 -0
- package/dist/web/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/web/assets/move-DB_GagMm.js +1 -0
- package/dist/web/assets/narrat-DLbgOhZU.js +1 -0
- package/dist/web/assets/nextflow-B0XVJmRM.js +1 -0
- package/dist/web/assets/nginx-D_VnBJ67.js +1 -0
- package/dist/web/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/web/assets/nim-ZlGxZxc3.js +1 -0
- package/dist/web/assets/nix-shcSOmrb.js +1 -0
- package/dist/web/assets/nord-Ddv68eIx.js +1 -0
- package/dist/web/assets/nushell-D4Tzg5kh.js +1 -0
- package/dist/web/assets/objective-c-Deuh7S70.js +1 -0
- package/dist/web/assets/objective-cpp-BUEGK8hf.js +1 -0
- package/dist/web/assets/ocaml-BNioltXt.js +1 -0
- package/dist/web/assets/one-dark-pro-GBQ2dnAY.js +1 -0
- package/dist/web/assets/one-light-PoHY5YXO.js +1 -0
- package/dist/web/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/web/assets/pascal-JqZropPD.js +1 -0
- package/dist/web/assets/pegDiagram-VL7TDLO6-CCQpyXjO.js +1 -0
- package/dist/web/assets/perl-CHQXSrWU.js +1 -0
- package/dist/web/assets/php-B5ebYQev.js +1 -0
- package/dist/web/assets/pieDiagram-7S7Q4E2Y-CZ_Lof4Z.js +39 -0
- package/dist/web/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/web/assets/plsql-LKU2TuZ1.js +1 -0
- package/dist/web/assets/po-BFLt1xDp.js +1 -0
- package/dist/web/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/web/assets/polar-DKykz6zU.js +1 -0
- package/dist/web/assets/postcss-B3ZDOciz.js +1 -0
- package/dist/web/assets/powerquery-CSHBycmS.js +1 -0
- package/dist/web/assets/powershell-BIEUsx6d.js +1 -0
- package/dist/web/assets/prisma-B48N-Iqd.js +1 -0
- package/dist/web/assets/prolog-BY-TUvya.js +1 -0
- package/dist/web/assets/proto-zocC4JxJ.js +1 -0
- package/dist/web/assets/pug-CM9l7STV.js +1 -0
- package/dist/web/assets/puppet-Cza_XSSt.js +1 -0
- package/dist/web/assets/purescript-Bg-kzb6g.js +1 -0
- package/dist/web/assets/python-DhUJRlN_.js +1 -0
- package/dist/web/assets/qml-D8XfuvdV.js +1 -0
- package/dist/web/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/web/assets/qss-DhMKtDLN.js +1 -0
- package/dist/web/assets/quadrantDiagram-CIZ2JOQS-hJaOjkKi.js +7 -0
- package/dist/web/assets/r-CwjWoCRV.js +1 -0
- package/dist/web/assets/racket-CzouJOBO.js +1 -0
- package/dist/web/assets/railroadDiagram-AXF67PYL-D9ueOsqF.js +1 -0
- package/dist/web/assets/raku-B1bQXN8T.js +1 -0
- package/dist/web/assets/razor-CNLDkMZG.js +1 -0
- package/dist/web/assets/red-bN70gL4F.js +1 -0
- package/dist/web/assets/reg-5LuOXUq_.js +1 -0
- package/dist/web/assets/regexp-DWJ3fJO_.js +1 -0
- package/dist/web/assets/rel-DJlmqQ1C.js +1 -0
- package/dist/web/assets/requirementDiagram-LRYGKXZP-CmMkOQTX.js +84 -0
- package/dist/web/assets/riscv-QhoSD0DR.js +1 -0
- package/dist/web/assets/rose-pine-CmCqftbK.js +1 -0
- package/dist/web/assets/rose-pine-dawn-Ds-gbosJ.js +1 -0
- package/dist/web/assets/rose-pine-moon-CjDtw9vr.js +1 -0
- package/dist/web/assets/rst-4NLicBqY.js +1 -0
- package/dist/web/assets/ruby-DeZ3UC14.js +1 -0
- package/dist/web/assets/rust-Be6lgOlo.js +1 -0
- package/dist/web/assets/sankeyDiagram-W5VNT64P-CXMCYDJB.js +40 -0
- package/dist/web/assets/sas-BmTFh92c.js +1 -0
- package/dist/web/assets/sass-BJ4Li9vH.js +1 -0
- package/dist/web/assets/scala-DQVVAn-B.js +1 -0
- package/dist/web/assets/scheme-BJGe-b2p.js +1 -0
- package/dist/web/assets/scss-C31hgJw-.js +1 -0
- package/dist/web/assets/sdbl-BLhTXw86.js +1 -0
- package/dist/web/assets/sequenceDiagram-SI44F4Z6-iT0A9JkH.js +162 -0
- package/dist/web/assets/shaderlab-B7qAK45m.js +1 -0
- package/dist/web/assets/shellscript-atvbtKCR.js +1 -0
- package/dist/web/assets/shellsession-C_rIy8kc.js +1 -0
- package/dist/web/assets/sizeCapture-X5ZJPWSS-mUYstuDe.js +1 -0
- package/dist/web/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/web/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/web/assets/smalltalk-DkLiglaE.js +1 -0
- package/dist/web/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/web/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/web/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/web/assets/solidity-C1w2a3ep.js +1 -0
- package/dist/web/assets/soy-C-lX7w71.js +1 -0
- package/dist/web/assets/sparql-bYkjHRlG.js +1 -0
- package/dist/web/assets/splunk-Cf8iN4DR.js +1 -0
- package/dist/web/assets/sql-COK4E0Yg.js +1 -0
- package/dist/web/assets/ssh-config-BknIz3MU.js +1 -0
- package/dist/web/assets/stata-DorPZHa4.js +1 -0
- package/dist/web/assets/stateDiagram-OKZ733FA-B7jqZn_1.js +1 -0
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-5syMa302.js +1 -0
- package/dist/web/assets/stylus-BeQkCIfX.js +1 -0
- package/dist/web/assets/svelte-MSaWC3Je.js +1 -0
- package/dist/web/assets/swift-BSxZ-RaX.js +1 -0
- package/dist/web/assets/swimlanes-SLNWSIFB-B3iAV6cM.js +2 -0
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CwnN5SDz.js +8 -0
- package/dist/web/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/web/assets/system-verilog-C7L56vO4.js +1 -0
- package/dist/web/assets/systemd-CUnW07Te.js +1 -0
- package/dist/web/assets/talonscript-C1XDQQGZ.js +1 -0
- package/dist/web/assets/tasl-CQjiPCtT.js +1 -0
- package/dist/web/assets/tcl-DQ1-QYvQ.js +1 -0
- package/dist/web/assets/templ-dwX3ZSMB.js +1 -0
- package/dist/web/assets/terraform-BbSNqyBO.js +1 -0
- package/dist/web/assets/tex-rYs2v40G.js +1 -0
- package/dist/web/assets/timeline-definition-Z64GVDOM-KdcKK-sf.js +120 -0
- package/dist/web/assets/tokyo-night-DBQeEorK.js +1 -0
- package/dist/web/assets/toml-CB2ApiWb.js +1 -0
- package/dist/web/assets/ts-tags-CipyTH0X.js +1 -0
- package/dist/web/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/web/assets/tsx-B6W0miNI.js +1 -0
- package/dist/web/assets/turtle-BMR_PYu6.js +1 -0
- package/dist/web/assets/twig-NC5TFiHP.js +1 -0
- package/dist/web/assets/typescript-Dj6nwHGl.js +1 -0
- package/dist/web/assets/typespec-BpWG_bgh.js +1 -0
- package/dist/web/assets/typst-BVUVsWT6.js +1 -0
- package/dist/web/assets/v-CAQ2eGtk.js +1 -0
- package/dist/web/assets/vala-BFOHcciG.js +1 -0
- package/dist/web/assets/vb-CdO5JTpU.js +1 -0
- package/dist/web/assets/vennDiagram-T6HMQDX7-rJqmKTgU.js +34 -0
- package/dist/web/assets/verilog-CJaU5se_.js +1 -0
- package/dist/web/assets/vesper-BEBZ7ncR.js +1 -0
- package/dist/web/assets/vhdl-DYoNaHQp.js +1 -0
- package/dist/web/assets/viml-m4uW47V2.js +1 -0
- package/dist/web/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/web/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/web/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/web/assets/vue-BuYVFjOK.js +1 -0
- package/dist/web/assets/vue-html-xdeiXROB.js +1 -0
- package/dist/web/assets/vyper-nyqBNV6O.js +1 -0
- package/dist/web/assets/wardleyDiagram-T6FBY63Y-CI33_pUs.js +78 -0
- package/dist/web/assets/wasm-C6j12Q_x.js +1 -0
- package/dist/web/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/web/assets/wenyan-7A4Fjokl.js +1 -0
- package/dist/web/assets/wgsl-CB0Krxn9.js +1 -0
- package/dist/web/assets/wikitext-DCE3LsBG.js +1 -0
- package/dist/web/assets/wolfram-C3FkfJm5.js +1 -0
- package/dist/web/assets/xml-e3z08dGr.js +1 -0
- package/dist/web/assets/xsl-Dd0NUgwM.js +1 -0
- package/dist/web/assets/xychartDiagram-ELKLHX3M-C1TnCm9i.js +7 -0
- package/dist/web/assets/yaml-CVw76BM1.js +1 -0
- package/dist/web/assets/zenscript-HnGAYVZD.js +1 -0
- package/dist/web/assets/zig-BVz_zdnA.js +1 -0
- package/dist/web/casper.png +0 -0
- package/dist/web/index.html +27 -0
- package/dist/web/logo.svg +30 -0
- package/dist/web/manifest.webmanifest +1 -0
- package/dist/web/registerSW.js +1 -0
- package/dist/web/sw.js +1 -0
- package/dist/web/workbox-9c191d2f.js +1 -0
- package/package.json +61 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,3506 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// server/src/paths.ts
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
function fromEnv(name) {
|
|
15
|
+
const v = process.env[name];
|
|
16
|
+
return v === void 0 || v.trim() === "" ? void 0 : v;
|
|
17
|
+
}
|
|
18
|
+
function configFilePath() {
|
|
19
|
+
const base = fromEnv("XDG_CONFIG_HOME") ?? path.join(os.homedir(), ".config");
|
|
20
|
+
return path.join(base, "casper", "config.json");
|
|
21
|
+
}
|
|
22
|
+
function dataDirPath() {
|
|
23
|
+
return fromEnv("CASPER_DATA_DIR") ?? path.join(os.homedir(), ".casper");
|
|
24
|
+
}
|
|
25
|
+
var init_paths = __esm({
|
|
26
|
+
"server/src/paths.ts"() {
|
|
27
|
+
"use strict";
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// server/src/cli/settings.ts
|
|
32
|
+
import crypto from "node:crypto";
|
|
33
|
+
import fs from "node:fs";
|
|
34
|
+
import path2 from "node:path";
|
|
35
|
+
function readSettings(file) {
|
|
36
|
+
try {
|
|
37
|
+
const doc = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
38
|
+
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return {};
|
|
39
|
+
return doc;
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function updateSettings(file, changes) {
|
|
45
|
+
const merged = { ...readSettings(file), ...changes };
|
|
46
|
+
fs.mkdirSync(path2.dirname(file), { recursive: true });
|
|
47
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
|
+
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
|
|
49
|
+
`, { mode: 384 });
|
|
50
|
+
fs.renameSync(tmp, file);
|
|
51
|
+
}
|
|
52
|
+
function generateToken() {
|
|
53
|
+
return crypto.randomBytes(24).toString("hex");
|
|
54
|
+
}
|
|
55
|
+
function printTokenBlock(token, heading) {
|
|
56
|
+
const line = "\u2500".repeat(Math.max(heading.length, token.length) + 2);
|
|
57
|
+
process.stdout.write(
|
|
58
|
+
`
|
|
59
|
+
\u250C${line}\u2510
|
|
60
|
+
\u2502 ${heading.padEnd(line.length - 2)} \u2502
|
|
61
|
+
\u2502 ${"".padEnd(line.length - 2)} \u2502
|
|
62
|
+
\u2502 ${token.padEnd(line.length - 2)} \u2502
|
|
63
|
+
\u2514${line}\u2518
|
|
64
|
+
|
|
65
|
+
`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
var init_settings = __esm({
|
|
69
|
+
"server/src/cli/settings.ts"() {
|
|
70
|
+
"use strict";
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// server/src/cli/agentFile.ts
|
|
75
|
+
var agentFile_exports = {};
|
|
76
|
+
__export(agentFile_exports, {
|
|
77
|
+
installAgentFile: () => installAgentFile
|
|
78
|
+
});
|
|
79
|
+
import crypto2 from "node:crypto";
|
|
80
|
+
import fs2 from "node:fs";
|
|
81
|
+
import path3 from "node:path";
|
|
82
|
+
import url from "node:url";
|
|
83
|
+
function sourceFile() {
|
|
84
|
+
const here = path3.dirname(url.fileURLToPath(import.meta.url));
|
|
85
|
+
const packaged = path3.resolve(here, "agents/casper.json");
|
|
86
|
+
const workspace = path3.resolve(here, "../../../assets/agents/casper.json");
|
|
87
|
+
return fs2.existsSync(packaged) ? packaged : workspace;
|
|
88
|
+
}
|
|
89
|
+
function sha256(text) {
|
|
90
|
+
return crypto2.createHash("sha256").update(text).digest("hex");
|
|
91
|
+
}
|
|
92
|
+
function installAgentFile(home2, dataDir) {
|
|
93
|
+
const src = sourceFile();
|
|
94
|
+
const target = path3.join(home2, ".kiro", "agents", "casper.json");
|
|
95
|
+
if (!fs2.existsSync(src)) return { action: "no-source", target };
|
|
96
|
+
const desired = fs2.readFileSync(src, "utf8");
|
|
97
|
+
const stampFile = path3.join(dataDir, "agent-stamp");
|
|
98
|
+
const stamp = fs2.existsSync(stampFile) ? fs2.readFileSync(stampFile, "utf8").trim() : "";
|
|
99
|
+
const write = () => {
|
|
100
|
+
fs2.mkdirSync(path3.dirname(target), { recursive: true });
|
|
101
|
+
fs2.writeFileSync(target, desired);
|
|
102
|
+
fs2.mkdirSync(dataDir, { recursive: true });
|
|
103
|
+
fs2.writeFileSync(stampFile, `${sha256(desired)}
|
|
104
|
+
`);
|
|
105
|
+
};
|
|
106
|
+
let current;
|
|
107
|
+
try {
|
|
108
|
+
current = fs2.lstatSync(target);
|
|
109
|
+
} catch {
|
|
110
|
+
current = void 0;
|
|
111
|
+
}
|
|
112
|
+
if (!current) {
|
|
113
|
+
write();
|
|
114
|
+
return { action: "installed", target };
|
|
115
|
+
}
|
|
116
|
+
if (current.isSymbolicLink()) {
|
|
117
|
+
fs2.rmSync(target);
|
|
118
|
+
write();
|
|
119
|
+
return { action: "updated", target };
|
|
120
|
+
}
|
|
121
|
+
const onDisk = fs2.readFileSync(target, "utf8");
|
|
122
|
+
if (sha256(onDisk) === sha256(desired)) return { action: "unchanged", target };
|
|
123
|
+
if (stamp !== "" && sha256(onDisk) === stamp) {
|
|
124
|
+
write();
|
|
125
|
+
return { action: "updated", target };
|
|
126
|
+
}
|
|
127
|
+
return { action: "kept-yours", target };
|
|
128
|
+
}
|
|
129
|
+
var init_agentFile = __esm({
|
|
130
|
+
"server/src/cli/agentFile.ts"() {
|
|
131
|
+
"use strict";
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// server/src/util/logger.ts
|
|
136
|
+
import pino from "pino";
|
|
137
|
+
var logger;
|
|
138
|
+
var init_logger = __esm({
|
|
139
|
+
"server/src/util/logger.ts"() {
|
|
140
|
+
"use strict";
|
|
141
|
+
logger = pino({
|
|
142
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
143
|
+
transport: process.env.NODE_ENV === "production" ? void 0 : {
|
|
144
|
+
target: "pino/file",
|
|
145
|
+
options: { destination: 2 }
|
|
146
|
+
// stderr, so stdout stays clean
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// server/src/config.ts
|
|
153
|
+
import { execFileSync } from "node:child_process";
|
|
154
|
+
import fs3 from "node:fs";
|
|
155
|
+
import os2 from "node:os";
|
|
156
|
+
import path4 from "node:path";
|
|
157
|
+
import url2 from "node:url";
|
|
158
|
+
function env(name, fallback) {
|
|
159
|
+
const v = process.env[name];
|
|
160
|
+
return v === void 0 || v === "" ? fallback : v;
|
|
161
|
+
}
|
|
162
|
+
function defaultWebDist() {
|
|
163
|
+
const here = path4.dirname(url2.fileURLToPath(import.meta.url));
|
|
164
|
+
const bundled = path4.join(here, "web");
|
|
165
|
+
const workspace = path4.resolve(here, "../../web/dist");
|
|
166
|
+
return fs3.existsSync(bundled) ? bundled : workspace;
|
|
167
|
+
}
|
|
168
|
+
function parseConfigDoc(raw, onWarn = () => {
|
|
169
|
+
}) {
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = JSON.parse(raw);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
onWarn("config: invalid JSON, ignoring the file entirely", err);
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
178
|
+
onWarn("config: expected a JSON object, ignoring");
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
const obj = parsed;
|
|
182
|
+
const unknown = Object.keys(obj).filter((k) => !KNOWN_KEYS.has(k));
|
|
183
|
+
if (unknown.length > 0) onWarn("config: ignoring unrecognised keys", unknown);
|
|
184
|
+
return obj;
|
|
185
|
+
}
|
|
186
|
+
function loadConfigFile() {
|
|
187
|
+
const file = configFilePath();
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = fs3.readFileSync(file, "utf8");
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (err.code !== "ENOENT") {
|
|
193
|
+
logger.warn({ file, err }, "config: unreadable, falling back to defaults");
|
|
194
|
+
}
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
const obj = parseConfigDoc(raw, (msg, detail) => logger.warn({ file, detail }, msg));
|
|
198
|
+
if (typeof obj.token === "string" && obj.token !== "") {
|
|
199
|
+
try {
|
|
200
|
+
const mode = fs3.statSync(file).mode & 63;
|
|
201
|
+
if (mode !== 0) {
|
|
202
|
+
logger.warn({ file }, "config: holds a token but is readable by others; chmod 600 it");
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return obj;
|
|
208
|
+
}
|
|
209
|
+
function pickString(fromEnv2, fromFile, fallback) {
|
|
210
|
+
if (fromEnv2 !== void 0 && fromEnv2 !== "") return fromEnv2;
|
|
211
|
+
if (typeof fromFile === "string" && fromFile !== "") return fromFile;
|
|
212
|
+
return fallback;
|
|
213
|
+
}
|
|
214
|
+
function pickInt(fromEnv2, fromFile, fallback) {
|
|
215
|
+
if (fromEnv2 !== void 0 && fromEnv2 !== "") {
|
|
216
|
+
const n = Number.parseInt(fromEnv2, 10);
|
|
217
|
+
if (Number.isFinite(n)) return n;
|
|
218
|
+
}
|
|
219
|
+
if (typeof fromFile === "number" && Number.isFinite(fromFile)) return fromFile;
|
|
220
|
+
if (typeof fromFile === "string") {
|
|
221
|
+
const n = Number.parseInt(fromFile, 10);
|
|
222
|
+
if (Number.isFinite(n)) return n;
|
|
223
|
+
}
|
|
224
|
+
return fallback;
|
|
225
|
+
}
|
|
226
|
+
function resolveKiroBin(explicit, home2) {
|
|
227
|
+
if (explicit.includes("/") && fs3.existsSync(explicit)) return explicit;
|
|
228
|
+
try {
|
|
229
|
+
const found = execFileSync("/bin/sh", ["-lc", `command -v ${explicit}`], {
|
|
230
|
+
encoding: "utf8"
|
|
231
|
+
}).trim();
|
|
232
|
+
if (found && fs3.existsSync(found)) return found;
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
const candidates = [
|
|
236
|
+
path4.join(home2, ".toolbox", "bin", explicit),
|
|
237
|
+
path4.join(home2, ".local", "bin", explicit),
|
|
238
|
+
path4.join("/usr", "local", "bin", explicit),
|
|
239
|
+
path4.join("/opt", "homebrew", "bin", explicit)
|
|
240
|
+
];
|
|
241
|
+
for (const c of candidates) {
|
|
242
|
+
if (fs3.existsSync(c)) return c;
|
|
243
|
+
}
|
|
244
|
+
return explicit;
|
|
245
|
+
}
|
|
246
|
+
var home, KNOWN_KEYS, fileConfig, setting, settingInt, config;
|
|
247
|
+
var init_config = __esm({
|
|
248
|
+
"server/src/config.ts"() {
|
|
249
|
+
"use strict";
|
|
250
|
+
init_paths();
|
|
251
|
+
init_logger();
|
|
252
|
+
home = os2.homedir();
|
|
253
|
+
KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
254
|
+
"host",
|
|
255
|
+
"port",
|
|
256
|
+
"token",
|
|
257
|
+
"sessionTtlSeconds",
|
|
258
|
+
"kiroBin",
|
|
259
|
+
"defaultCwd",
|
|
260
|
+
"maxLiveSessions",
|
|
261
|
+
"defaultAgent",
|
|
262
|
+
"fileRoot",
|
|
263
|
+
"eventBufferSize",
|
|
264
|
+
"maxUploadBytes"
|
|
265
|
+
]);
|
|
266
|
+
fileConfig = loadConfigFile();
|
|
267
|
+
setting = (envName, key, fallback) => pickString(process.env[envName], fileConfig[key], fallback);
|
|
268
|
+
settingInt = (envName, key, fallback) => pickInt(process.env[envName], fileConfig[key], fallback);
|
|
269
|
+
config = {
|
|
270
|
+
host: setting("HOST", "host", "0.0.0.0"),
|
|
271
|
+
port: settingInt("PORT", "port", 4319),
|
|
272
|
+
/** Shared-secret token required to log in. Empty string disables auth (dev only). */
|
|
273
|
+
token: setting("CASPER_TOKEN", "token", ""),
|
|
274
|
+
/** Device-login lifetime in seconds; slid forward on activity. Default 7 days. */
|
|
275
|
+
sessionTtlSeconds: settingInt("CASPER_SESSION_TTL_SECONDS", "sessionTtlSeconds", 60 * 60 * 24 * 7),
|
|
276
|
+
kiroBin: resolveKiroBin(setting("KIRO_BIN", "kiroBin", "kiro-cli"), home),
|
|
277
|
+
defaultCwd: setting("DEFAULT_CWD", "defaultCwd", process.cwd()),
|
|
278
|
+
maxLiveSessions: settingInt("MAX_LIVE_SESSIONS", "maxLiveSessions", 6),
|
|
279
|
+
defaultAgent: setting("DEFAULT_AGENT", "defaultAgent", "kiro_default"),
|
|
280
|
+
/**
|
|
281
|
+
* Filesystem root that file-serving endpoints (/api/fs/dirs, /api/fs/image)
|
|
282
|
+
* are confined to. Requests resolving outside this root are rejected.
|
|
283
|
+
* Defaults to the filesystem root (/), so file browsing spans everything the
|
|
284
|
+
* server process can read. Set CASPER_FILE_ROOT to a narrower path (e.g. the
|
|
285
|
+
* user's home directory) to stop authenticated users from reading system
|
|
286
|
+
* files such as /etc or SSH keys.
|
|
287
|
+
*/
|
|
288
|
+
fileRoot: path4.resolve(setting("CASPER_FILE_ROOT", "fileRoot", "/")),
|
|
289
|
+
/** Directory where kiro-cli persists its own session files. */
|
|
290
|
+
kiroSessionsDir: path4.join(home, ".kiro", "sessions", "cli"),
|
|
291
|
+
/** Where casper.db lives. Env-only: it says where data is, and the config
|
|
292
|
+
* file isn't there. */
|
|
293
|
+
casperDataDir: dataDirPath(),
|
|
294
|
+
/** Built web app to serve. Env-only: install layout, not a user preference. */
|
|
295
|
+
webDist: env("CASPER_WEB_DIST", defaultWebDist()),
|
|
296
|
+
/** Per-session in-memory event ring buffer size. */
|
|
297
|
+
eventBufferSize: settingInt("EVENT_BUFFER_SIZE", "eventBufferSize", 5e3),
|
|
298
|
+
/** Max size (bytes) for a single uploaded file. Default 100 MB. */
|
|
299
|
+
maxUploadBytes: settingInt("CASPER_MAX_UPLOAD_BYTES", "maxUploadBytes", 100 * 1024 * 1024),
|
|
300
|
+
/** Absolute path of the settings file, for diagnostics. */
|
|
301
|
+
configFile: configFilePath()
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// server/src/session/db.ts
|
|
307
|
+
import fs4 from "node:fs";
|
|
308
|
+
import path5 from "node:path";
|
|
309
|
+
import { DatabaseSync } from "node:sqlite";
|
|
310
|
+
function db() {
|
|
311
|
+
if (!handle) handle = open();
|
|
312
|
+
return handle;
|
|
313
|
+
}
|
|
314
|
+
function open() {
|
|
315
|
+
fs4.mkdirSync(config.casperDataDir, { recursive: true, mode: 448 });
|
|
316
|
+
const file = path5.join(config.casperDataDir, "casper.db");
|
|
317
|
+
const d = new DatabaseSync(file);
|
|
318
|
+
d.exec("PRAGMA journal_mode = WAL");
|
|
319
|
+
d.exec(SCHEMA);
|
|
320
|
+
restrict(config.casperDataDir, 448);
|
|
321
|
+
for (const f of [file, `${file}-wal`, `${file}-shm`]) restrict(f, 384);
|
|
322
|
+
return d;
|
|
323
|
+
}
|
|
324
|
+
function restrict(target, mode) {
|
|
325
|
+
try {
|
|
326
|
+
if ((fs4.statSync(target).mode & 511) !== mode) fs4.chmodSync(target, mode);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
var SCHEMA, handle;
|
|
331
|
+
var init_db = __esm({
|
|
332
|
+
"server/src/session/db.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
init_config();
|
|
335
|
+
SCHEMA = `
|
|
336
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
337
|
+
session_id TEXT PRIMARY KEY,
|
|
338
|
+
title TEXT,
|
|
339
|
+
cwd TEXT
|
|
340
|
+
);
|
|
341
|
+
CREATE TABLE IF NOT EXISTS logins (
|
|
342
|
+
id TEXT PRIMARY KEY,
|
|
343
|
+
hash TEXT NOT NULL UNIQUE,
|
|
344
|
+
created_at TEXT NOT NULL,
|
|
345
|
+
last_seen_at TEXT NOT NULL,
|
|
346
|
+
user_agent TEXT
|
|
347
|
+
);
|
|
348
|
+
CREATE INDEX IF NOT EXISTS logins_hash ON logins (hash);
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// server/src/session/logins.ts
|
|
354
|
+
var logins_exports = {};
|
|
355
|
+
__export(logins_exports, {
|
|
356
|
+
LoginStore: () => LoginStore
|
|
357
|
+
});
|
|
358
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
359
|
+
function sha2562(s) {
|
|
360
|
+
return createHash("sha256").update(s).digest("hex");
|
|
361
|
+
}
|
|
362
|
+
function sameHash(a, b) {
|
|
363
|
+
if (a.length !== b.length) return false;
|
|
364
|
+
return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
365
|
+
}
|
|
366
|
+
var LAST_SEEN_WRITE_INTERVAL_MS, str, toRecord, LoginStore;
|
|
367
|
+
var init_logins = __esm({
|
|
368
|
+
"server/src/session/logins.ts"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
init_config();
|
|
371
|
+
init_db();
|
|
372
|
+
LAST_SEEN_WRITE_INTERVAL_MS = 6e4;
|
|
373
|
+
str = (v) => typeof v === "string" ? v : "";
|
|
374
|
+
toRecord = (r) => ({
|
|
375
|
+
id: str(r.id),
|
|
376
|
+
hash: str(r.hash),
|
|
377
|
+
createdAt: str(r.created_at),
|
|
378
|
+
lastSeenAt: str(r.last_seen_at),
|
|
379
|
+
userAgent: typeof r.user_agent === "string" ? r.user_agent : void 0
|
|
380
|
+
});
|
|
381
|
+
LoginStore = class {
|
|
382
|
+
constructor() {
|
|
383
|
+
this.pruneExpired();
|
|
384
|
+
}
|
|
385
|
+
ttlMs() {
|
|
386
|
+
return config.sessionTtlSeconds * 1e3;
|
|
387
|
+
}
|
|
388
|
+
/** Drop logins whose last activity is older than the TTL. */
|
|
389
|
+
pruneExpired(now = Date.now()) {
|
|
390
|
+
const cutoff = new Date(now - this.ttlMs()).toISOString();
|
|
391
|
+
db().prepare("DELETE FROM logins WHERE last_seen_at < ?").run(cutoff);
|
|
392
|
+
}
|
|
393
|
+
/** Create a login. Returns the raw token to set as the cookie value. */
|
|
394
|
+
create(userAgent) {
|
|
395
|
+
const token = randomBytes(32).toString("base64url");
|
|
396
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
397
|
+
const record = {
|
|
398
|
+
id: randomBytes(9).toString("base64url"),
|
|
399
|
+
hash: sha2562(token),
|
|
400
|
+
createdAt: nowIso,
|
|
401
|
+
lastSeenAt: nowIso,
|
|
402
|
+
userAgent
|
|
403
|
+
};
|
|
404
|
+
db().prepare(
|
|
405
|
+
`INSERT INTO logins (id, hash, created_at, last_seen_at, user_agent)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
407
|
+
).run(record.id, record.hash, nowIso, nowIso, userAgent ?? null);
|
|
408
|
+
return { token, record };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Verify a raw token. Returns the record (sliding its expiry forward) or null
|
|
412
|
+
* if unknown/expired. Writes the lastSeen bump at most once a minute.
|
|
413
|
+
*/
|
|
414
|
+
verify(token) {
|
|
415
|
+
if (!token) return null;
|
|
416
|
+
const now = Date.now();
|
|
417
|
+
const row = db().prepare("SELECT * FROM logins WHERE hash = ?").get(sha2562(token));
|
|
418
|
+
if (!row) return null;
|
|
419
|
+
const record = toRecord(row);
|
|
420
|
+
if (now - Date.parse(record.lastSeenAt) > this.ttlMs()) {
|
|
421
|
+
db().prepare("DELETE FROM logins WHERE id = ?").run(record.id);
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (now - Date.parse(record.lastSeenAt) >= LAST_SEEN_WRITE_INTERVAL_MS) {
|
|
425
|
+
record.lastSeenAt = new Date(now).toISOString();
|
|
426
|
+
db().prepare("UPDATE logins SET last_seen_at = ? WHERE id = ?").run(record.lastSeenAt, record.id);
|
|
427
|
+
}
|
|
428
|
+
return record;
|
|
429
|
+
}
|
|
430
|
+
/** List all active devices, marking the one owning `currentToken`. */
|
|
431
|
+
list(currentToken) {
|
|
432
|
+
this.pruneExpired();
|
|
433
|
+
const currentHash = currentToken ? sha2562(currentToken) : void 0;
|
|
434
|
+
const rows = db().prepare("SELECT * FROM logins ORDER BY last_seen_at DESC").all();
|
|
435
|
+
return rows.map(toRecord).map((r) => ({
|
|
436
|
+
id: r.id,
|
|
437
|
+
createdAt: r.createdAt,
|
|
438
|
+
lastSeenAt: r.lastSeenAt,
|
|
439
|
+
userAgent: r.userAgent,
|
|
440
|
+
current: currentHash !== void 0 && sameHash(r.hash, currentHash)
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
/** Revoke the device holding this token (used on logout). */
|
|
444
|
+
revokeToken(token) {
|
|
445
|
+
if (!token) return;
|
|
446
|
+
db().prepare("DELETE FROM logins WHERE hash = ?").run(sha2562(token));
|
|
447
|
+
}
|
|
448
|
+
/** Revoke a device by its public id. Returns false if there was no such device. */
|
|
449
|
+
revokeId(id) {
|
|
450
|
+
const res = db().prepare("DELETE FROM logins WHERE id = ?").run(id);
|
|
451
|
+
return res.changes > 0;
|
|
452
|
+
}
|
|
453
|
+
/** Log out every device. */
|
|
454
|
+
revokeAll() {
|
|
455
|
+
db().prepare("DELETE FROM logins").run();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// server/src/cli/service.ts
|
|
462
|
+
var service_exports = {};
|
|
463
|
+
__export(service_exports, {
|
|
464
|
+
hasUserSystemd: () => hasUserSystemd,
|
|
465
|
+
serviceActive: () => serviceActive,
|
|
466
|
+
serviceInstall: () => serviceInstall,
|
|
467
|
+
serviceStatus: () => serviceStatus,
|
|
468
|
+
serviceUninstall: () => serviceUninstall
|
|
469
|
+
});
|
|
470
|
+
import { execFileSync as execFileSync2, spawnSync } from "node:child_process";
|
|
471
|
+
import fs5 from "node:fs";
|
|
472
|
+
import os3 from "node:os";
|
|
473
|
+
import path6 from "node:path";
|
|
474
|
+
import url3 from "node:url";
|
|
475
|
+
function unitPath() {
|
|
476
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || path6.join(os3.homedir(), ".config");
|
|
477
|
+
return path6.join(base, "systemd", "user", SERVICE);
|
|
478
|
+
}
|
|
479
|
+
function systemctl(args, quiet = false) {
|
|
480
|
+
const r = spawnSync("systemctl", ["--user", ...args], {
|
|
481
|
+
stdio: quiet ? "ignore" : "inherit"
|
|
482
|
+
});
|
|
483
|
+
return r.status ?? 1;
|
|
484
|
+
}
|
|
485
|
+
function hasUserSystemd() {
|
|
486
|
+
const r = spawnSync("systemctl", ["--user", "show-environment"], { stdio: "ignore" });
|
|
487
|
+
return r.status === 0;
|
|
488
|
+
}
|
|
489
|
+
function entryScript() {
|
|
490
|
+
const here = path6.dirname(url3.fileURLToPath(import.meta.url));
|
|
491
|
+
const bundled = path6.join(here, "casper.js");
|
|
492
|
+
if (fs5.existsSync(bundled)) return bundled;
|
|
493
|
+
return path6.resolve(here, "../index.js");
|
|
494
|
+
}
|
|
495
|
+
function serviceInstall() {
|
|
496
|
+
if (!hasUserSystemd()) {
|
|
497
|
+
process.stderr.write(
|
|
498
|
+
"casper: no user systemd here. Run Casper with `casper`, or start it from your own init system.\n"
|
|
499
|
+
);
|
|
500
|
+
return 1;
|
|
501
|
+
}
|
|
502
|
+
const unit = unitPath();
|
|
503
|
+
fs5.mkdirSync(path6.dirname(unit), { recursive: true });
|
|
504
|
+
fs5.writeFileSync(
|
|
505
|
+
unit,
|
|
506
|
+
`[Unit]
|
|
507
|
+
Description=Casper (kiro-cli web client)
|
|
508
|
+
After=network-online.target
|
|
509
|
+
Wants=network-online.target
|
|
510
|
+
|
|
511
|
+
[Service]
|
|
512
|
+
Type=simple
|
|
513
|
+
WorkingDirectory=${os3.homedir()}
|
|
514
|
+
Environment=NODE_ENV=production
|
|
515
|
+
ExecStart=${process.execPath} ${entryScript()}
|
|
516
|
+
Restart=on-failure
|
|
517
|
+
RestartSec=3
|
|
518
|
+
|
|
519
|
+
[Install]
|
|
520
|
+
WantedBy=default.target
|
|
521
|
+
`
|
|
522
|
+
);
|
|
523
|
+
process.stdout.write(`wrote ${unit}
|
|
524
|
+
`);
|
|
525
|
+
try {
|
|
526
|
+
execFileSync2("loginctl", ["enable-linger", os3.userInfo().username], { stdio: "ignore" });
|
|
527
|
+
} catch {
|
|
528
|
+
process.stderr.write(
|
|
529
|
+
`! could not enable linger; the service may stop at logout. Run: sudo loginctl enable-linger ${os3.userInfo().username}
|
|
530
|
+
`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
systemctl(["daemon-reload"]);
|
|
534
|
+
if (systemctl(["enable", "--now", SERVICE]) !== 0) {
|
|
535
|
+
process.stderr.write(`! could not start the service. Check: systemctl --user status ${SERVICE}
|
|
536
|
+
`);
|
|
537
|
+
return 1;
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write("casper is running as a systemd user service\n");
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
function serviceUninstall() {
|
|
543
|
+
if (!hasUserSystemd()) {
|
|
544
|
+
process.stderr.write("casper: no user systemd here; nothing to remove.\n");
|
|
545
|
+
return 1;
|
|
546
|
+
}
|
|
547
|
+
systemctl(["disable", "--now", SERVICE], true);
|
|
548
|
+
const unit = unitPath();
|
|
549
|
+
if (fs5.existsSync(unit)) {
|
|
550
|
+
fs5.rmSync(unit);
|
|
551
|
+
process.stdout.write(`removed ${unit}
|
|
552
|
+
`);
|
|
553
|
+
}
|
|
554
|
+
systemctl(["daemon-reload"], true);
|
|
555
|
+
process.stdout.write("service removed; your sessions and settings are untouched\n");
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
function serviceStatus() {
|
|
559
|
+
if (!hasUserSystemd()) {
|
|
560
|
+
process.stdout.write("no user systemd here; Casper is not running as a service\n");
|
|
561
|
+
return 0;
|
|
562
|
+
}
|
|
563
|
+
return systemctl(["status", "--no-pager", SERVICE]);
|
|
564
|
+
}
|
|
565
|
+
function serviceActive() {
|
|
566
|
+
return spawnSync("systemctl", ["--user", "is-active", "--quiet", SERVICE], { stdio: "ignore" }).status === 0;
|
|
567
|
+
}
|
|
568
|
+
var SERVICE;
|
|
569
|
+
var init_service = __esm({
|
|
570
|
+
"server/src/cli/service.ts"() {
|
|
571
|
+
"use strict";
|
|
572
|
+
SERVICE = "casper.service";
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// shared/dist/acp.js
|
|
577
|
+
function isJsonRpcResponse(m) {
|
|
578
|
+
return "id" in m && m.id !== void 0 && ("result" in m || "error" in m);
|
|
579
|
+
}
|
|
580
|
+
function isJsonRpcError(m) {
|
|
581
|
+
return "error" in m;
|
|
582
|
+
}
|
|
583
|
+
function isJsonRpcNotification(m) {
|
|
584
|
+
return !("id" in m) && "method" in m;
|
|
585
|
+
}
|
|
586
|
+
var ACP_METHODS, KIRO_NOTIFICATIONS;
|
|
587
|
+
var init_acp = __esm({
|
|
588
|
+
"shared/dist/acp.js"() {
|
|
589
|
+
"use strict";
|
|
590
|
+
ACP_METHODS = {
|
|
591
|
+
initialize: "initialize",
|
|
592
|
+
sessionNew: "session/new",
|
|
593
|
+
sessionLoad: "session/load",
|
|
594
|
+
sessionPrompt: "session/prompt",
|
|
595
|
+
sessionCancel: "session/cancel",
|
|
596
|
+
sessionSetMode: "session/set_mode",
|
|
597
|
+
sessionSetModel: "session/set_model",
|
|
598
|
+
sessionUpdate: "session/update",
|
|
599
|
+
commandsExecute: "_kiro.dev/commands/execute"
|
|
600
|
+
};
|
|
601
|
+
KIRO_NOTIFICATIONS = {
|
|
602
|
+
metadata: "_kiro.dev/metadata",
|
|
603
|
+
subagentListUpdate: "_kiro.dev/subagent/list_update",
|
|
604
|
+
mcpServerInitialized: "_kiro.dev/mcp/server_initialized",
|
|
605
|
+
mcpServerInitFailure: "_kiro.dev/mcp/server_init_failure",
|
|
606
|
+
commandsAvailable: "_kiro.dev/commands/available",
|
|
607
|
+
mcpOauthRequest: "_kiro.dev/mcp/oauth_request",
|
|
608
|
+
compactionStatus: "_kiro.dev/compaction/status"
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// shared/dist/attachments.js
|
|
614
|
+
function attachmentPaths(text) {
|
|
615
|
+
const line = text.split("\n").find((l) => l.startsWith(ATTACHMENTS_PREFIX));
|
|
616
|
+
if (!line)
|
|
617
|
+
return [];
|
|
618
|
+
return line.slice(ATTACHMENTS_PREFIX.length).split(", ").map((s) => s.trim()).filter(Boolean);
|
|
619
|
+
}
|
|
620
|
+
function imageAttachmentPaths(text) {
|
|
621
|
+
return attachmentPaths(text).filter((p) => {
|
|
622
|
+
const lower = p.toLowerCase();
|
|
623
|
+
return IMAGE_EXTS.some((ext) => lower.endsWith(ext));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function stripAttachmentsLine(text) {
|
|
627
|
+
return text.split("\n").filter((l) => !l.startsWith(ATTACHMENTS_PREFIX)).join("\n").trim();
|
|
628
|
+
}
|
|
629
|
+
var ATTACHMENTS_PREFIX, IMAGE_EXTS;
|
|
630
|
+
var init_attachments = __esm({
|
|
631
|
+
"shared/dist/attachments.js"() {
|
|
632
|
+
"use strict";
|
|
633
|
+
ATTACHMENTS_PREFIX = "Attached files: ";
|
|
634
|
+
IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico", ".avif"];
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// shared/dist/observability.js
|
|
639
|
+
function emptyObservabilitySnapshot() {
|
|
640
|
+
return {
|
|
641
|
+
turnStatus: "idle",
|
|
642
|
+
creditsSpent: 0,
|
|
643
|
+
lastTurnCredits: 0,
|
|
644
|
+
contextUsagePercentage: 0,
|
|
645
|
+
lastTurnDurationMs: 0,
|
|
646
|
+
subagents: [],
|
|
647
|
+
pendingStages: [],
|
|
648
|
+
mcpServers: [],
|
|
649
|
+
availableCommands: [],
|
|
650
|
+
oauthPrompts: [],
|
|
651
|
+
compacting: false
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
var init_observability = __esm({
|
|
655
|
+
"shared/dist/observability.js"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// shared/dist/rest-dto.js
|
|
661
|
+
var init_rest_dto = __esm({
|
|
662
|
+
"shared/dist/rest-dto.js"() {
|
|
663
|
+
"use strict";
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// shared/dist/ws-protocol.js
|
|
668
|
+
var init_ws_protocol = __esm({
|
|
669
|
+
"shared/dist/ws-protocol.js"() {
|
|
670
|
+
"use strict";
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// shared/dist/index.js
|
|
675
|
+
var init_dist = __esm({
|
|
676
|
+
"shared/dist/index.js"() {
|
|
677
|
+
"use strict";
|
|
678
|
+
init_acp();
|
|
679
|
+
init_attachments();
|
|
680
|
+
init_observability();
|
|
681
|
+
init_rest_dto();
|
|
682
|
+
init_ws_protocol();
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// server/src/util/paths.ts
|
|
687
|
+
import fs6 from "node:fs";
|
|
688
|
+
import fsp from "node:fs/promises";
|
|
689
|
+
import path7 from "node:path";
|
|
690
|
+
function isWithinRoot(root, target) {
|
|
691
|
+
const resolved = path7.resolve(target);
|
|
692
|
+
if (root === path7.sep) return true;
|
|
693
|
+
return resolved === root || resolved.startsWith(root + path7.sep);
|
|
694
|
+
}
|
|
695
|
+
function confineToRoot(root, input) {
|
|
696
|
+
const resolved = path7.resolve(root, input);
|
|
697
|
+
return isWithinRoot(root, resolved) ? resolved : null;
|
|
698
|
+
}
|
|
699
|
+
function resolveRealRoot(root) {
|
|
700
|
+
if (realRootCache && realRootCache.root === root) return realRootCache.real;
|
|
701
|
+
let real;
|
|
702
|
+
try {
|
|
703
|
+
real = fs6.realpathSync(root);
|
|
704
|
+
} catch {
|
|
705
|
+
real = root;
|
|
706
|
+
}
|
|
707
|
+
realRootCache = { root, real };
|
|
708
|
+
return real;
|
|
709
|
+
}
|
|
710
|
+
async function realConfineToRoot(root, absPath) {
|
|
711
|
+
let realTarget;
|
|
712
|
+
try {
|
|
713
|
+
realTarget = await fsp.realpath(absPath);
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
return isWithinRoot(resolveRealRoot(root), realTarget) ? realTarget : null;
|
|
718
|
+
}
|
|
719
|
+
function isValidSessionId(id) {
|
|
720
|
+
return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && !id.includes("..");
|
|
721
|
+
}
|
|
722
|
+
var realRootCache;
|
|
723
|
+
var init_paths2 = __esm({
|
|
724
|
+
"server/src/util/paths.ts"() {
|
|
725
|
+
"use strict";
|
|
726
|
+
realRootCache = null;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// server/src/acp/errors.ts
|
|
731
|
+
function describeError(error) {
|
|
732
|
+
const detail = detailText(error.data);
|
|
733
|
+
return detail ? `${detail} (${error.message}, code ${error.code})` : `${error.message} (code ${error.code})`;
|
|
734
|
+
}
|
|
735
|
+
function detailText(data) {
|
|
736
|
+
if (data == null) return "";
|
|
737
|
+
if (typeof data === "string") return truncate(data.trim());
|
|
738
|
+
if (typeof data === "number" || typeof data === "boolean") return String(data);
|
|
739
|
+
if (typeof data === "object") {
|
|
740
|
+
const rec = data;
|
|
741
|
+
for (const key of ["message", "error", "detail", "details", "reason", "cause"]) {
|
|
742
|
+
const v = rec[key];
|
|
743
|
+
if (typeof v === "string" && v.trim()) return truncate(v.trim());
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
return truncate(JSON.stringify(data));
|
|
747
|
+
} catch {
|
|
748
|
+
return "";
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return "";
|
|
752
|
+
}
|
|
753
|
+
var MAX_DETAIL, truncate;
|
|
754
|
+
var init_errors = __esm({
|
|
755
|
+
"server/src/acp/errors.ts"() {
|
|
756
|
+
"use strict";
|
|
757
|
+
MAX_DETAIL = 2e3;
|
|
758
|
+
truncate = (s) => s.length > MAX_DETAIL ? `${s.slice(0, MAX_DETAIL)}\u2026 (truncated)` : s;
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// server/src/acp/AcpClient.ts
|
|
763
|
+
import { EventEmitter } from "node:events";
|
|
764
|
+
import split2 from "split2";
|
|
765
|
+
var AcpClient;
|
|
766
|
+
var init_AcpClient = __esm({
|
|
767
|
+
"server/src/acp/AcpClient.ts"() {
|
|
768
|
+
"use strict";
|
|
769
|
+
init_dist();
|
|
770
|
+
init_errors();
|
|
771
|
+
AcpClient = class extends EventEmitter {
|
|
772
|
+
nextId = 1;
|
|
773
|
+
pending = /* @__PURE__ */ new Map();
|
|
774
|
+
writable;
|
|
775
|
+
log;
|
|
776
|
+
closed = false;
|
|
777
|
+
constructor(readable, writable, log) {
|
|
778
|
+
super();
|
|
779
|
+
this.writable = writable;
|
|
780
|
+
this.log = log;
|
|
781
|
+
readable.pipe(split2()).on("data", (line) => {
|
|
782
|
+
const trimmed = line.trim();
|
|
783
|
+
if (!trimmed) return;
|
|
784
|
+
let msg;
|
|
785
|
+
try {
|
|
786
|
+
msg = JSON.parse(trimmed);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
this.emit("parseError", err, trimmed);
|
|
789
|
+
this.log.warn({ line: trimmed.slice(0, 200) }, "acp: failed to parse line");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.handleMessage(msg);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
handleMessage(msg) {
|
|
796
|
+
if (isJsonRpcResponse(msg)) {
|
|
797
|
+
const id = "id" in msg ? msg.id : null;
|
|
798
|
+
if (id === null) return;
|
|
799
|
+
const pending = this.pending.get(id);
|
|
800
|
+
if (!pending) {
|
|
801
|
+
this.log.warn({ id }, "acp: response for unknown request id");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
this.pending.delete(id);
|
|
805
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
806
|
+
if (isJsonRpcError(msg)) {
|
|
807
|
+
pending.reject(new Error(`ACP ${pending.method} failed: ${describeError(msg.error)}`));
|
|
808
|
+
} else {
|
|
809
|
+
pending.resolve(msg.result);
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (isJsonRpcNotification(msg)) {
|
|
814
|
+
this.emit("notification", msg);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if ("id" in msg && "method" in msg) {
|
|
818
|
+
this.emit("serverRequest", msg);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/** Send a request and await its result. */
|
|
822
|
+
request(method, params, timeoutMs = 12e4) {
|
|
823
|
+
if (this.closed) return Promise.reject(new Error("ACP client is closed"));
|
|
824
|
+
const id = this.nextId++;
|
|
825
|
+
const payload = { jsonrpc: "2.0", id, method, params };
|
|
826
|
+
return new Promise((resolve, reject) => {
|
|
827
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
828
|
+
this.pending.delete(id);
|
|
829
|
+
reject(new Error(`ACP ${method} timed out after ${timeoutMs}ms`));
|
|
830
|
+
}, timeoutMs) : void 0;
|
|
831
|
+
this.pending.set(id, {
|
|
832
|
+
resolve,
|
|
833
|
+
reject,
|
|
834
|
+
method,
|
|
835
|
+
timer
|
|
836
|
+
});
|
|
837
|
+
this.write(payload);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
notify(method, params) {
|
|
841
|
+
if (this.closed) return;
|
|
842
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
843
|
+
}
|
|
844
|
+
respond(id, result) {
|
|
845
|
+
if (this.closed) return;
|
|
846
|
+
this.write({ jsonrpc: "2.0", id, result });
|
|
847
|
+
}
|
|
848
|
+
write(msg) {
|
|
849
|
+
this.writable.write(JSON.stringify(msg) + "\n");
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Reject all pending requests; call when the child process exits. The reason
|
|
853
|
+
* is passed through verbatim because it reaches the user as a turn_error -
|
|
854
|
+
* the caller supplies something readable, so don't bury it behind a prefix
|
|
855
|
+
* about ACP internals.
|
|
856
|
+
*/
|
|
857
|
+
fail(reason) {
|
|
858
|
+
this.closed = true;
|
|
859
|
+
for (const [id, pending] of this.pending) {
|
|
860
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
861
|
+
pending.reject(new Error(reason));
|
|
862
|
+
this.pending.delete(id);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// server/src/session/KiroProcess.ts
|
|
870
|
+
import { spawn } from "node:child_process";
|
|
871
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
872
|
+
import split22 from "split2";
|
|
873
|
+
var STDERR_KEEP, KiroProcess;
|
|
874
|
+
var init_KiroProcess = __esm({
|
|
875
|
+
"server/src/session/KiroProcess.ts"() {
|
|
876
|
+
"use strict";
|
|
877
|
+
init_dist();
|
|
878
|
+
init_config();
|
|
879
|
+
init_AcpClient();
|
|
880
|
+
STDERR_KEEP = 8;
|
|
881
|
+
KiroProcess = class extends EventEmitter2 {
|
|
882
|
+
client;
|
|
883
|
+
child;
|
|
884
|
+
log;
|
|
885
|
+
disposed = false;
|
|
886
|
+
recentStderr = [];
|
|
887
|
+
constructor(opts, log) {
|
|
888
|
+
super();
|
|
889
|
+
this.log = log;
|
|
890
|
+
const args = ["acp", "--trust-all-tools"];
|
|
891
|
+
if (opts.agent) args.push("--agent", opts.agent);
|
|
892
|
+
if (opts.model) args.push("--model", opts.model);
|
|
893
|
+
this.log.info({ bin: config.kiroBin, args, cwd: opts.cwd }, "spawning kiro-cli acp");
|
|
894
|
+
this.child = spawn(config.kiroBin, args, {
|
|
895
|
+
cwd: opts.cwd,
|
|
896
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
897
|
+
env: { ...process.env }
|
|
898
|
+
});
|
|
899
|
+
this.client = new AcpClient(this.child.stdout, this.child.stdin, log);
|
|
900
|
+
this.client.on("notification", (n) => {
|
|
901
|
+
this.emit("notification", n);
|
|
902
|
+
});
|
|
903
|
+
this.client.on("serverRequest", (req) => {
|
|
904
|
+
this.log.debug({ method: req.method }, "acp: unhandled server request");
|
|
905
|
+
this.client.respond(req.id, {});
|
|
906
|
+
});
|
|
907
|
+
this.child.stderr.pipe(split22()).on("data", (line) => {
|
|
908
|
+
if (!line.trim()) return;
|
|
909
|
+
this.log.debug({ stderr: line }, "kiro-cli stderr");
|
|
910
|
+
this.recentStderr.push(line.trim());
|
|
911
|
+
if (this.recentStderr.length > STDERR_KEEP) this.recentStderr.shift();
|
|
912
|
+
});
|
|
913
|
+
this.child.on("exit", (code2, signal) => {
|
|
914
|
+
this.client.fail(this.exitReason(code2, signal));
|
|
915
|
+
if (!this.disposed) {
|
|
916
|
+
this.log.warn({ code: code2, signal }, "kiro-cli acp exited unexpectedly");
|
|
917
|
+
}
|
|
918
|
+
this.emit("exit", code2, signal);
|
|
919
|
+
});
|
|
920
|
+
this.child.on("error", (err) => {
|
|
921
|
+
this.log.error({ err }, "kiro-cli acp spawn error");
|
|
922
|
+
this.client.fail(err.message);
|
|
923
|
+
});
|
|
924
|
+
this.child.stdin.on("error", () => {
|
|
925
|
+
});
|
|
926
|
+
this.child.stdout.on("error", () => {
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/** Perform the ACP initialize handshake. Idempotent. */
|
|
930
|
+
async initialize() {
|
|
931
|
+
const result = await this.client.request(ACP_METHODS.initialize, {
|
|
932
|
+
protocolVersion: 1,
|
|
933
|
+
clientCapabilities: {
|
|
934
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
935
|
+
terminal: false
|
|
936
|
+
},
|
|
937
|
+
// Informational only - kiro logs it and nothing reads it back, so it isn't
|
|
938
|
+
// worth wiring to package.json.
|
|
939
|
+
clientInfo: { name: "casper", version: "0.5.0" }
|
|
940
|
+
});
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
newSession(params) {
|
|
944
|
+
return this.client.request(ACP_METHODS.sessionNew, params);
|
|
945
|
+
}
|
|
946
|
+
loadSession(params) {
|
|
947
|
+
return this.client.request(ACP_METHODS.sessionLoad, params);
|
|
948
|
+
}
|
|
949
|
+
/** Run a prompt turn to completion. Resolves with the stop reason. */
|
|
950
|
+
prompt(params) {
|
|
951
|
+
return this.client.request(
|
|
952
|
+
ACP_METHODS.sessionPrompt,
|
|
953
|
+
params,
|
|
954
|
+
0
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
cancel(sessionId) {
|
|
958
|
+
this.client.notify(ACP_METHODS.sessionCancel, { sessionId });
|
|
959
|
+
}
|
|
960
|
+
setMode(sessionId, modeId) {
|
|
961
|
+
return this.client.request(ACP_METHODS.sessionSetMode, { sessionId, modeId });
|
|
962
|
+
}
|
|
963
|
+
setModel(sessionId, modelId) {
|
|
964
|
+
return this.client.request(ACP_METHODS.sessionSetModel, { sessionId, modelId });
|
|
965
|
+
}
|
|
966
|
+
execCommand(sessionId, command, args) {
|
|
967
|
+
return this.client.request(ACP_METHODS.commandsExecute, {
|
|
968
|
+
sessionId,
|
|
969
|
+
command: { command: command.replace(/^\//, ""), args: args ? { input: args } : {} }
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/** Cleanly shut down: close stdin (triggers kiro's graceful exit), then kill. */
|
|
973
|
+
/**
|
|
974
|
+
* Recent stderr from kiro, for attaching to a failure.
|
|
975
|
+
*
|
|
976
|
+
* A failed request doesn't always carry its cause in the JSON-RPC error - when
|
|
977
|
+
* it doesn't, kiro has usually written something here. Shared across turns, so
|
|
978
|
+
* treat it as context rather than proof.
|
|
979
|
+
*/
|
|
980
|
+
stderrTail() {
|
|
981
|
+
return this.recentStderr.join("\n").trim();
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Why the child died, in terms a user can act on. The exit code alone is
|
|
985
|
+
* opaque, so append what kiro printed - that's where "credentials have
|
|
986
|
+
* expired, run kiro-cli login" and friends show up.
|
|
987
|
+
*/
|
|
988
|
+
exitReason(code2, signal) {
|
|
989
|
+
const base = signal ? `kiro-cli exited on ${signal}` : `kiro-cli exited with code ${code2}`;
|
|
990
|
+
const tail = this.recentStderr.join("\n").trim();
|
|
991
|
+
return tail ? `${base}: ${tail}` : base;
|
|
992
|
+
}
|
|
993
|
+
dispose() {
|
|
994
|
+
if (this.disposed) return;
|
|
995
|
+
this.disposed = true;
|
|
996
|
+
try {
|
|
997
|
+
this.child.stdin.end();
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
setTimeout(() => {
|
|
1001
|
+
if (this.child.exitCode === null) this.child.kill("SIGTERM");
|
|
1002
|
+
}, 1e3).unref();
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Dispose and resolve once the child has actually exited. kiro writes its
|
|
1006
|
+
* session file on shutdown, so a caller that then deletes those files must
|
|
1007
|
+
* wait for exit first, or kiro's write recreates them.
|
|
1008
|
+
*/
|
|
1009
|
+
disposeAndWait(timeoutMs = 4e3) {
|
|
1010
|
+
if (this.child.exitCode !== null) {
|
|
1011
|
+
this.dispose();
|
|
1012
|
+
return Promise.resolve();
|
|
1013
|
+
}
|
|
1014
|
+
return new Promise((resolve) => {
|
|
1015
|
+
let settled = false;
|
|
1016
|
+
const finish = () => {
|
|
1017
|
+
if (settled) return;
|
|
1018
|
+
settled = true;
|
|
1019
|
+
resolve();
|
|
1020
|
+
};
|
|
1021
|
+
this.child.once("exit", finish);
|
|
1022
|
+
this.dispose();
|
|
1023
|
+
setTimeout(finish, timeoutMs).unref();
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// server/src/session/EventStore.ts
|
|
1031
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
1032
|
+
var EventStore;
|
|
1033
|
+
var init_EventStore = __esm({
|
|
1034
|
+
"server/src/session/EventStore.ts"() {
|
|
1035
|
+
"use strict";
|
|
1036
|
+
init_config();
|
|
1037
|
+
EventStore = class extends EventEmitter3 {
|
|
1038
|
+
sessionId;
|
|
1039
|
+
buffer = [];
|
|
1040
|
+
capacity;
|
|
1041
|
+
seq = 0;
|
|
1042
|
+
constructor(sessionId) {
|
|
1043
|
+
super();
|
|
1044
|
+
this.sessionId = sessionId;
|
|
1045
|
+
this.capacity = config.eventBufferSize;
|
|
1046
|
+
}
|
|
1047
|
+
/** Append an event, assign it the next seq, and fan it out. */
|
|
1048
|
+
append(payload) {
|
|
1049
|
+
this.seq += 1;
|
|
1050
|
+
const event = {
|
|
1051
|
+
seq: this.seq,
|
|
1052
|
+
ts: Date.now(),
|
|
1053
|
+
sessionId: this.sessionId,
|
|
1054
|
+
payload
|
|
1055
|
+
};
|
|
1056
|
+
this.buffer.push(event);
|
|
1057
|
+
if (this.buffer.length > this.capacity) this.buffer.shift();
|
|
1058
|
+
this.emit("event", event);
|
|
1059
|
+
return event;
|
|
1060
|
+
}
|
|
1061
|
+
/** Highest assigned seq. Clients start their cursor here after a full refetch. */
|
|
1062
|
+
head() {
|
|
1063
|
+
return this.seq;
|
|
1064
|
+
}
|
|
1065
|
+
/** Oldest seq still in the buffer (0 if empty). */
|
|
1066
|
+
tail() {
|
|
1067
|
+
return this.buffer.length > 0 ? this.buffer[0].seq : 0;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Events with seq > cursor, in order. Returns { events, gap } - gap is true
|
|
1071
|
+
* when the cursor is older than the buffer tail, meaning some events were
|
|
1072
|
+
* evicted and the client must resync (full transcript refetch).
|
|
1073
|
+
*/
|
|
1074
|
+
getSince(cursor) {
|
|
1075
|
+
if (this.buffer.length === 0) {
|
|
1076
|
+
return { events: [], gap: cursor > 0 };
|
|
1077
|
+
}
|
|
1078
|
+
const tail = this.tail();
|
|
1079
|
+
const gap = cursor > 0 && cursor < tail - 1;
|
|
1080
|
+
const events = this.buffer.filter((e) => e.seq > cursor);
|
|
1081
|
+
return { events, gap };
|
|
1082
|
+
}
|
|
1083
|
+
dispose() {
|
|
1084
|
+
this.removeAllListeners();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// server/src/session/TurnState.ts
|
|
1091
|
+
var TurnState;
|
|
1092
|
+
var init_TurnState = __esm({
|
|
1093
|
+
"server/src/session/TurnState.ts"() {
|
|
1094
|
+
"use strict";
|
|
1095
|
+
init_dist();
|
|
1096
|
+
TurnState = class {
|
|
1097
|
+
snapshot = emptyObservabilitySnapshot();
|
|
1098
|
+
get() {
|
|
1099
|
+
return this.snapshot;
|
|
1100
|
+
}
|
|
1101
|
+
/** Apply one event; returns the updated snapshot. */
|
|
1102
|
+
apply(payload) {
|
|
1103
|
+
const s = this.snapshot;
|
|
1104
|
+
switch (payload.kind) {
|
|
1105
|
+
case "turn_started": {
|
|
1106
|
+
this.snapshot = { ...s, turnStatus: "running", currentTurnStartedAt: Date.now() };
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
case "turn_ended": {
|
|
1110
|
+
this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
case "turn_error": {
|
|
1114
|
+
this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
case "process_exited": {
|
|
1118
|
+
this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "metadata": {
|
|
1122
|
+
const p = payload.params;
|
|
1123
|
+
const turnCredits = (p.meteringUsage ?? []).reduce((sum, m) => sum + m.value, 0);
|
|
1124
|
+
this.snapshot = {
|
|
1125
|
+
...s,
|
|
1126
|
+
contextUsagePercentage: p.contextUsagePercentage ?? s.contextUsagePercentage,
|
|
1127
|
+
// meteringUsage is emitted per-turn; accumulate into the session total.
|
|
1128
|
+
creditsSpent: turnCredits > 0 ? s.creditsSpent + turnCredits : s.creditsSpent,
|
|
1129
|
+
lastTurnCredits: turnCredits > 0 ? turnCredits : s.lastTurnCredits,
|
|
1130
|
+
lastTurnDurationMs: p.turnDurationMs ?? s.lastTurnDurationMs
|
|
1131
|
+
};
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
case "subagent_update": {
|
|
1135
|
+
this.snapshot = {
|
|
1136
|
+
...s,
|
|
1137
|
+
subagents: payload.params.subagents,
|
|
1138
|
+
pendingStages: payload.params.pendingStages
|
|
1139
|
+
};
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
case "mcp_health": {
|
|
1143
|
+
const { serverName, error } = payload.params;
|
|
1144
|
+
const status = payload.ok ? "initialized" : "failed";
|
|
1145
|
+
const next = s.mcpServers.filter((m) => m.serverName !== serverName);
|
|
1146
|
+
next.push({ serverName, status, error, updatedAt: Date.now() });
|
|
1147
|
+
this.snapshot = { ...s, mcpServers: next };
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
case "commands_available": {
|
|
1151
|
+
this.snapshot = { ...s, availableCommands: payload.params.commands };
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "compaction": {
|
|
1155
|
+
this.snapshot = { ...s, compacting: payload.params.status.type === "started" };
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
case "oauth_request": {
|
|
1159
|
+
const prompts = [
|
|
1160
|
+
...s.oauthPrompts,
|
|
1161
|
+
{
|
|
1162
|
+
serverName: payload.params.serverName,
|
|
1163
|
+
url: payload.params.url,
|
|
1164
|
+
createdAt: Date.now()
|
|
1165
|
+
}
|
|
1166
|
+
];
|
|
1167
|
+
this.snapshot = { ...s, oauthPrompts: prompts };
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1170
|
+
default:
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
return this.snapshot;
|
|
1174
|
+
}
|
|
1175
|
+
/** Seed cumulative credits/context from kiro's persisted metadata on resume. */
|
|
1176
|
+
seed(creditsSpent, contextUsagePercentage) {
|
|
1177
|
+
this.snapshot = { ...this.snapshot, creditsSpent, contextUsagePercentage };
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// server/src/session/kiroFiles.ts
|
|
1184
|
+
import fs7 from "node:fs/promises";
|
|
1185
|
+
import path8 from "node:path";
|
|
1186
|
+
function isContentBlock(v) {
|
|
1187
|
+
return isRecord(v) && typeof v.kind === "string";
|
|
1188
|
+
}
|
|
1189
|
+
function toolUseData(b) {
|
|
1190
|
+
if (b.kind !== "toolUse" || !isRecord(b.data)) return null;
|
|
1191
|
+
const { toolUseId, name, input } = b.data;
|
|
1192
|
+
if (typeof toolUseId !== "string") return null;
|
|
1193
|
+
return { toolUseId, name: typeof name === "string" ? name : void 0, input };
|
|
1194
|
+
}
|
|
1195
|
+
function toolResultData(b) {
|
|
1196
|
+
if (b.kind !== "toolResult" || !isRecord(b.data)) return null;
|
|
1197
|
+
const { toolUseId, status, content } = b.data;
|
|
1198
|
+
if (typeof toolUseId !== "string") return null;
|
|
1199
|
+
return { toolUseId, status: typeof status === "string" ? status : void 0, content };
|
|
1200
|
+
}
|
|
1201
|
+
function contentBlocks(content) {
|
|
1202
|
+
return Array.isArray(content) ? content.filter(isContentBlock) : [];
|
|
1203
|
+
}
|
|
1204
|
+
function blockText(c) {
|
|
1205
|
+
if (typeof c.data === "string") return c.data;
|
|
1206
|
+
if (isRecord(c.data) && typeof c.data.text === "string") return c.data.text;
|
|
1207
|
+
return "";
|
|
1208
|
+
}
|
|
1209
|
+
function renderableBlocks(content) {
|
|
1210
|
+
return contentBlocks(content).filter((b) => b.kind !== "image");
|
|
1211
|
+
}
|
|
1212
|
+
function summarize(j) {
|
|
1213
|
+
const state = j.session_state;
|
|
1214
|
+
const turns = state?.conversation_metadata?.user_turn_metadatas ?? [];
|
|
1215
|
+
const creditsSpent = turns.reduce(
|
|
1216
|
+
(sum, t) => sum + (t.metering_usage ?? []).reduce((a, m) => a + m.value, 0),
|
|
1217
|
+
0
|
|
1218
|
+
);
|
|
1219
|
+
const contextUsagePercentage = state?.rts_model_state?.context_usage_percentage ?? turns[turns.length - 1]?.context_usage_percentage;
|
|
1220
|
+
return {
|
|
1221
|
+
sessionId: j.session_id,
|
|
1222
|
+
title: j.title?.trim() || "Untitled session",
|
|
1223
|
+
cwd: j.cwd,
|
|
1224
|
+
createdAt: j.created_at,
|
|
1225
|
+
updatedAt: j.updated_at,
|
|
1226
|
+
liveness: "dormant",
|
|
1227
|
+
agentId: state?.agent_name,
|
|
1228
|
+
modelId: state?.rts_model_state?.model_info?.model_id,
|
|
1229
|
+
running: false,
|
|
1230
|
+
creditsSpent,
|
|
1231
|
+
contextUsagePercentage
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
async function listPersistedSessions(log) {
|
|
1235
|
+
let files;
|
|
1236
|
+
try {
|
|
1237
|
+
files = await fs7.readdir(config.kiroSessionsDir);
|
|
1238
|
+
} catch {
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
1242
|
+
const summaries = [];
|
|
1243
|
+
await Promise.all(
|
|
1244
|
+
jsonFiles.map(async (f) => {
|
|
1245
|
+
try {
|
|
1246
|
+
const raw = await fs7.readFile(path8.join(config.kiroSessionsDir, f), "utf8");
|
|
1247
|
+
summaries.push(summarize(JSON.parse(raw)));
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
log.debug({ err, f }, "kiroFiles: skipping unreadable session file");
|
|
1250
|
+
}
|
|
1251
|
+
})
|
|
1252
|
+
);
|
|
1253
|
+
summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1254
|
+
return summaries;
|
|
1255
|
+
}
|
|
1256
|
+
async function deletePersistedSession(sessionId) {
|
|
1257
|
+
if (!isValidSessionId(sessionId)) return;
|
|
1258
|
+
const targets = [
|
|
1259
|
+
path8.join(config.kiroSessionsDir, `${sessionId}.json`),
|
|
1260
|
+
path8.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
|
|
1261
|
+
path8.join(config.kiroSessionsDir, `${sessionId}.history`),
|
|
1262
|
+
path8.join(config.kiroSessionsDir, sessionId)
|
|
1263
|
+
];
|
|
1264
|
+
await Promise.all(targets.map((f) => fs7.rm(f, { recursive: true, force: true })));
|
|
1265
|
+
}
|
|
1266
|
+
async function readPersistedSession(sessionId) {
|
|
1267
|
+
if (!isValidSessionId(sessionId)) return null;
|
|
1268
|
+
try {
|
|
1269
|
+
const raw = await fs7.readFile(
|
|
1270
|
+
path8.join(config.kiroSessionsDir, `${sessionId}.json`),
|
|
1271
|
+
"utf8"
|
|
1272
|
+
);
|
|
1273
|
+
return summarize(JSON.parse(raw));
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async function hydrateTranscript(sessionId) {
|
|
1279
|
+
if (!isValidSessionId(sessionId)) return [];
|
|
1280
|
+
let raw;
|
|
1281
|
+
try {
|
|
1282
|
+
raw = await fs7.readFile(
|
|
1283
|
+
path8.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
|
|
1284
|
+
"utf8"
|
|
1285
|
+
);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return [];
|
|
1288
|
+
}
|
|
1289
|
+
const items = [];
|
|
1290
|
+
const toolsById = /* @__PURE__ */ new Map();
|
|
1291
|
+
for (const line of raw.split("\n")) {
|
|
1292
|
+
const trimmed = line.trim();
|
|
1293
|
+
if (!trimmed) continue;
|
|
1294
|
+
let entry;
|
|
1295
|
+
try {
|
|
1296
|
+
entry = JSON.parse(trimmed);
|
|
1297
|
+
} catch {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
const content = contentBlocks(entry.data.content);
|
|
1301
|
+
const textOf = (kind) => content.filter((c) => c.kind === kind).map(blockText).join("");
|
|
1302
|
+
const baseId = entry.data.message_id ?? `${items.length}`;
|
|
1303
|
+
const ts = entry.data.meta?.timestamp;
|
|
1304
|
+
const pushMsg = (msg) => items.push({ type: "message", message: msg });
|
|
1305
|
+
if (entry.kind === "Prompt") {
|
|
1306
|
+
const raw2 = textOf("text");
|
|
1307
|
+
const text = stripAttachmentsLine(raw2);
|
|
1308
|
+
const imagePaths = imageAttachmentPaths(raw2);
|
|
1309
|
+
if (text.trim() || imagePaths.length)
|
|
1310
|
+
pushMsg({ id: `u-${baseId}`, role: "user", text, timestamp: ts, imagePaths });
|
|
1311
|
+
} else if (entry.kind === "AssistantMessage") {
|
|
1312
|
+
const thinking = textOf("thinking");
|
|
1313
|
+
if (thinking.trim())
|
|
1314
|
+
pushMsg({ id: `t-${baseId}`, role: "thinking", text: thinking, timestamp: ts });
|
|
1315
|
+
const text = textOf("text");
|
|
1316
|
+
if (text.trim())
|
|
1317
|
+
pushMsg({ id: `a-${baseId}`, role: "assistant", text, timestamp: ts });
|
|
1318
|
+
for (const c of content) {
|
|
1319
|
+
const d = toolUseData(c);
|
|
1320
|
+
if (!d) continue;
|
|
1321
|
+
const tool = {
|
|
1322
|
+
id: d.toolUseId,
|
|
1323
|
+
name: d.name,
|
|
1324
|
+
title: d.name ?? d.toolUseId,
|
|
1325
|
+
status: "completed",
|
|
1326
|
+
input: d.input,
|
|
1327
|
+
content: []
|
|
1328
|
+
};
|
|
1329
|
+
toolsById.set(d.toolUseId, tool);
|
|
1330
|
+
items.push({ type: "tool_call", tool });
|
|
1331
|
+
}
|
|
1332
|
+
} else if (entry.kind === "ToolResults") {
|
|
1333
|
+
for (const c of content) {
|
|
1334
|
+
const d = toolResultData(c);
|
|
1335
|
+
if (!d) continue;
|
|
1336
|
+
const tool = toolsById.get(d.toolUseId);
|
|
1337
|
+
if (!tool) continue;
|
|
1338
|
+
tool.status = d.status === "error" ? "failed" : "completed";
|
|
1339
|
+
tool.content = renderableBlocks(d.content);
|
|
1340
|
+
}
|
|
1341
|
+
} else if (entry.kind === "Compaction") {
|
|
1342
|
+
const summary = entry.data.summary ?? "";
|
|
1343
|
+
if (summary.trim())
|
|
1344
|
+
items.push({ type: "compaction", id: `c-${baseId}`, summary, timestamp: ts });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return items;
|
|
1348
|
+
}
|
|
1349
|
+
var isRecord;
|
|
1350
|
+
var init_kiroFiles = __esm({
|
|
1351
|
+
"server/src/session/kiroFiles.ts"() {
|
|
1352
|
+
"use strict";
|
|
1353
|
+
init_dist();
|
|
1354
|
+
init_config();
|
|
1355
|
+
init_paths2();
|
|
1356
|
+
isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// server/src/session/sessionStore.ts
|
|
1361
|
+
var SessionStore;
|
|
1362
|
+
var init_sessionStore = __esm({
|
|
1363
|
+
"server/src/session/sessionStore.ts"() {
|
|
1364
|
+
"use strict";
|
|
1365
|
+
init_db();
|
|
1366
|
+
SessionStore = class {
|
|
1367
|
+
getTitle(sessionId) {
|
|
1368
|
+
return this.read(sessionId, "title");
|
|
1369
|
+
}
|
|
1370
|
+
setTitle(sessionId, title) {
|
|
1371
|
+
this.write(sessionId, "title", title);
|
|
1372
|
+
}
|
|
1373
|
+
getCwd(sessionId) {
|
|
1374
|
+
return this.read(sessionId, "cwd");
|
|
1375
|
+
}
|
|
1376
|
+
setCwd(sessionId, cwd) {
|
|
1377
|
+
this.write(sessionId, "cwd", cwd);
|
|
1378
|
+
}
|
|
1379
|
+
/** Forget every override for a session, when it's permanently deleted. */
|
|
1380
|
+
remove(sessionId) {
|
|
1381
|
+
db().prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
|
|
1382
|
+
}
|
|
1383
|
+
read(sessionId, column) {
|
|
1384
|
+
const row = db().prepare(`SELECT ${column} AS value FROM sessions WHERE session_id = ?`).get(sessionId);
|
|
1385
|
+
return row?.value ?? void 0;
|
|
1386
|
+
}
|
|
1387
|
+
write(sessionId, column, value) {
|
|
1388
|
+
db().prepare(
|
|
1389
|
+
`INSERT INTO sessions (session_id, ${column}) VALUES (?, ?)
|
|
1390
|
+
ON CONFLICT(session_id) DO UPDATE SET ${column} = excluded.${column}`
|
|
1391
|
+
).run(sessionId, value);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// server/src/session/SessionManager.ts
|
|
1398
|
+
import fs8 from "node:fs";
|
|
1399
|
+
import path9 from "node:path";
|
|
1400
|
+
function resolveCwd(input) {
|
|
1401
|
+
const raw = input?.trim();
|
|
1402
|
+
const abs = raw ? path9.resolve(config.defaultCwd, raw) : config.defaultCwd;
|
|
1403
|
+
if (!isWithinRoot(config.fileRoot, abs)) {
|
|
1404
|
+
throw new Error(`Working directory is outside the allowed root: ${abs}`);
|
|
1405
|
+
}
|
|
1406
|
+
let stat;
|
|
1407
|
+
try {
|
|
1408
|
+
stat = fs8.statSync(abs);
|
|
1409
|
+
} catch {
|
|
1410
|
+
stat = void 0;
|
|
1411
|
+
}
|
|
1412
|
+
if (stat && !stat.isDirectory()) {
|
|
1413
|
+
throw new Error(`Working directory path is a file, not a directory: ${abs}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (!stat) {
|
|
1416
|
+
fs8.mkdirSync(abs, { recursive: true });
|
|
1417
|
+
}
|
|
1418
|
+
return abs;
|
|
1419
|
+
}
|
|
1420
|
+
function mapNotification(n) {
|
|
1421
|
+
switch (n.method) {
|
|
1422
|
+
case "session/update":
|
|
1423
|
+
return { kind: "session_update", update: n.params.update };
|
|
1424
|
+
case KIRO_NOTIFICATIONS.metadata:
|
|
1425
|
+
return { kind: "metadata", params: n.params };
|
|
1426
|
+
case KIRO_NOTIFICATIONS.subagentListUpdate:
|
|
1427
|
+
return { kind: "subagent_update", params: n.params };
|
|
1428
|
+
case KIRO_NOTIFICATIONS.mcpServerInitialized:
|
|
1429
|
+
return { kind: "mcp_health", params: n.params, ok: true };
|
|
1430
|
+
case KIRO_NOTIFICATIONS.mcpServerInitFailure:
|
|
1431
|
+
return { kind: "mcp_health", params: n.params, ok: false };
|
|
1432
|
+
case KIRO_NOTIFICATIONS.commandsAvailable:
|
|
1433
|
+
return { kind: "commands_available", params: n.params };
|
|
1434
|
+
case KIRO_NOTIFICATIONS.compactionStatus:
|
|
1435
|
+
return { kind: "compaction", params: n.params };
|
|
1436
|
+
case KIRO_NOTIFICATIONS.mcpOauthRequest:
|
|
1437
|
+
return { kind: "oauth_request", params: n.params };
|
|
1438
|
+
default:
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
var Session, TRANSCRIPT_PAGE_SIZE, SessionManager;
|
|
1443
|
+
var init_SessionManager = __esm({
|
|
1444
|
+
"server/src/session/SessionManager.ts"() {
|
|
1445
|
+
"use strict";
|
|
1446
|
+
init_dist();
|
|
1447
|
+
init_config();
|
|
1448
|
+
init_paths2();
|
|
1449
|
+
init_KiroProcess();
|
|
1450
|
+
init_EventStore();
|
|
1451
|
+
init_TurnState();
|
|
1452
|
+
init_kiroFiles();
|
|
1453
|
+
init_sessionStore();
|
|
1454
|
+
Session = class {
|
|
1455
|
+
sessionId;
|
|
1456
|
+
store;
|
|
1457
|
+
turnState = new TurnState();
|
|
1458
|
+
cwd;
|
|
1459
|
+
agentId;
|
|
1460
|
+
modelId;
|
|
1461
|
+
currentModeId;
|
|
1462
|
+
availableModes = [];
|
|
1463
|
+
title = "New session";
|
|
1464
|
+
createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1465
|
+
updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1466
|
+
lastActivity = Date.now();
|
|
1467
|
+
running = false;
|
|
1468
|
+
// True once kiro has created or loaded this session id.
|
|
1469
|
+
everLive = false;
|
|
1470
|
+
proc;
|
|
1471
|
+
// In-flight spawn, so concurrent actions share one process.
|
|
1472
|
+
spawning;
|
|
1473
|
+
// True while kiro is replaying history during session/load. The transcript is
|
|
1474
|
+
// already hydrated from disk, so replayed notifications must not be appended
|
|
1475
|
+
// to the live store (doing so floods the chat with duplicate tool calls).
|
|
1476
|
+
replaying = false;
|
|
1477
|
+
constructor(sessionId, store, cwd) {
|
|
1478
|
+
this.sessionId = sessionId;
|
|
1479
|
+
this.store = store;
|
|
1480
|
+
this.cwd = cwd;
|
|
1481
|
+
}
|
|
1482
|
+
markLive() {
|
|
1483
|
+
this.everLive = true;
|
|
1484
|
+
}
|
|
1485
|
+
get hasBeenLive() {
|
|
1486
|
+
return this.everLive;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Single append path: fold the event into the live snapshot AND persist/fan
|
|
1490
|
+
* it out. Using this everywhere keeps turnState in sync with the event log,
|
|
1491
|
+
* so a client that refetches mid-turn sees turnStatus 'running' rather than
|
|
1492
|
+
* a stale 'idle'.
|
|
1493
|
+
*/
|
|
1494
|
+
record(payload) {
|
|
1495
|
+
this.turnState.apply(payload);
|
|
1496
|
+
return this.store.append(payload);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
TRANSCRIPT_PAGE_SIZE = 80;
|
|
1500
|
+
SessionManager = class {
|
|
1501
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1502
|
+
log;
|
|
1503
|
+
store = new SessionStore();
|
|
1504
|
+
constructor(log) {
|
|
1505
|
+
this.log = log;
|
|
1506
|
+
}
|
|
1507
|
+
/** Set a user title override for a session. */
|
|
1508
|
+
renameSession(sessionId, title) {
|
|
1509
|
+
this.store.setTitle(sessionId, title);
|
|
1510
|
+
const s = this.sessions.get(sessionId);
|
|
1511
|
+
if (s) s.title = title.trim() || s.title;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Re-point a session at a different working directory. A session's cwd is
|
|
1515
|
+
* fixed at creation, so this is needed when the original folder was moved or
|
|
1516
|
+
* deleted (the workspace endpoints and the kiro process are scoped to it).
|
|
1517
|
+
*
|
|
1518
|
+
* The target is validated and created exactly like a new session's cwd, so a
|
|
1519
|
+
* folder is only created on this explicit action - never silently on resume.
|
|
1520
|
+
* Any live kiro process was spawned with the old cwd, so it's disposed here;
|
|
1521
|
+
* the next turn respawns in the new directory with the transcript intact
|
|
1522
|
+
* (kiro reloads the session from its own file).
|
|
1523
|
+
*
|
|
1524
|
+
* Returns the resolved absolute path.
|
|
1525
|
+
*/
|
|
1526
|
+
async setSessionCwd(sessionId, input) {
|
|
1527
|
+
const resolved = resolveCwd(input);
|
|
1528
|
+
const s = await this.ensureOpen(sessionId);
|
|
1529
|
+
this.store.setCwd(sessionId, resolved);
|
|
1530
|
+
if (s.cwd !== resolved) {
|
|
1531
|
+
s.cwd = resolved;
|
|
1532
|
+
s.proc?.dispose();
|
|
1533
|
+
s.proc = void 0;
|
|
1534
|
+
this.log.info({ sessionId, cwd: resolved }, "session working directory changed");
|
|
1535
|
+
}
|
|
1536
|
+
return resolved;
|
|
1537
|
+
}
|
|
1538
|
+
get liveCount() {
|
|
1539
|
+
let n = 0;
|
|
1540
|
+
for (const s of this.sessions.values()) if (s.proc) n++;
|
|
1541
|
+
return n;
|
|
1542
|
+
}
|
|
1543
|
+
// -------------------------------------------------------------------------
|
|
1544
|
+
// Event subscription - works for any opened session, spawned or not.
|
|
1545
|
+
// -------------------------------------------------------------------------
|
|
1546
|
+
onEvent(sessionId, cb) {
|
|
1547
|
+
const s = this.sessions.get(sessionId);
|
|
1548
|
+
if (!s) return null;
|
|
1549
|
+
s.store.on("event", cb);
|
|
1550
|
+
return () => s.store.off("event", cb);
|
|
1551
|
+
}
|
|
1552
|
+
getStore(sessionId) {
|
|
1553
|
+
return this.sessions.get(sessionId)?.store;
|
|
1554
|
+
}
|
|
1555
|
+
/** Get a session's working directory. Opens the session in memory if needed. */
|
|
1556
|
+
async getSessionCwd(sessionId) {
|
|
1557
|
+
const s = await this.ensureOpen(sessionId);
|
|
1558
|
+
return s.cwd;
|
|
1559
|
+
}
|
|
1560
|
+
/** Open a session in memory (store + metadata) WITHOUT spawning a process. */
|
|
1561
|
+
async ensureOpen(sessionId) {
|
|
1562
|
+
const existing = this.sessions.get(sessionId);
|
|
1563
|
+
if (existing) return existing;
|
|
1564
|
+
const persisted = await readPersistedSession(sessionId);
|
|
1565
|
+
if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
|
|
1566
|
+
const effectiveCwd = this.store.getCwd(sessionId) ?? persisted.cwd;
|
|
1567
|
+
if (!isWithinRoot(config.fileRoot, effectiveCwd)) {
|
|
1568
|
+
throw new Error(
|
|
1569
|
+
`Session working directory is outside the allowed root: ${effectiveCwd}`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const store = new EventStore(sessionId);
|
|
1573
|
+
const s = new Session(sessionId, store, effectiveCwd);
|
|
1574
|
+
s.title = persisted.title;
|
|
1575
|
+
s.agentId = persisted.agentId;
|
|
1576
|
+
s.currentModeId = persisted.agentId;
|
|
1577
|
+
s.modelId = persisted.modelId;
|
|
1578
|
+
s.createdAt = persisted.createdAt;
|
|
1579
|
+
s.updatedAt = persisted.updatedAt;
|
|
1580
|
+
s.markLive();
|
|
1581
|
+
s.turnState.seed(
|
|
1582
|
+
persisted.creditsSpent ?? 0,
|
|
1583
|
+
persisted.contextUsagePercentage ?? 0
|
|
1584
|
+
);
|
|
1585
|
+
this.sessions.set(sessionId, s);
|
|
1586
|
+
return s;
|
|
1587
|
+
}
|
|
1588
|
+
// -------------------------------------------------------------------------
|
|
1589
|
+
// Lazy process spawning
|
|
1590
|
+
// -------------------------------------------------------------------------
|
|
1591
|
+
wire(s, proc) {
|
|
1592
|
+
proc.on("notification", (n) => {
|
|
1593
|
+
if (s.replaying) return;
|
|
1594
|
+
const payload = mapNotification(n);
|
|
1595
|
+
if (!payload) return;
|
|
1596
|
+
s.record(payload);
|
|
1597
|
+
});
|
|
1598
|
+
proc.on("exit", (code2, signal) => {
|
|
1599
|
+
if (s.proc !== proc) return;
|
|
1600
|
+
s.record({ kind: "process_exited", code: code2, signal });
|
|
1601
|
+
s.proc = void 0;
|
|
1602
|
+
s.running = false;
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
/** Get (or spawn + initialize + create/load) the kiro process for a session. */
|
|
1606
|
+
async ensureProc(s) {
|
|
1607
|
+
if (s.proc) return s.proc;
|
|
1608
|
+
if (s.spawning) return s.spawning;
|
|
1609
|
+
s.spawning = (async () => {
|
|
1610
|
+
await this.ensureCapacity();
|
|
1611
|
+
const proc = new KiroProcess(
|
|
1612
|
+
{ cwd: s.cwd, agent: s.agentId, model: s.modelId },
|
|
1613
|
+
this.log
|
|
1614
|
+
);
|
|
1615
|
+
this.wire(s, proc);
|
|
1616
|
+
await proc.initialize();
|
|
1617
|
+
let res;
|
|
1618
|
+
if (s.hasBeenLive) {
|
|
1619
|
+
s.replaying = true;
|
|
1620
|
+
try {
|
|
1621
|
+
res = await proc.loadSession({ sessionId: s.sessionId, cwd: s.cwd, mcpServers: [] });
|
|
1622
|
+
} finally {
|
|
1623
|
+
s.replaying = false;
|
|
1624
|
+
}
|
|
1625
|
+
} else {
|
|
1626
|
+
res = await proc.newSession({ cwd: s.cwd, mcpServers: [] });
|
|
1627
|
+
}
|
|
1628
|
+
if (!s.hasBeenLive && res.sessionId !== s.sessionId) {
|
|
1629
|
+
this.sessions.delete(s.sessionId);
|
|
1630
|
+
s.sessionId = res.sessionId;
|
|
1631
|
+
this.sessions.set(res.sessionId, s);
|
|
1632
|
+
}
|
|
1633
|
+
s.availableModes = res.modes.availableModes;
|
|
1634
|
+
s.currentModeId = res.modes.currentModeId;
|
|
1635
|
+
s.agentId = res.modes.currentModeId ?? s.agentId;
|
|
1636
|
+
s.markLive();
|
|
1637
|
+
s.proc = proc;
|
|
1638
|
+
s.spawning = void 0;
|
|
1639
|
+
return proc;
|
|
1640
|
+
})();
|
|
1641
|
+
try {
|
|
1642
|
+
return await s.spawning;
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
s.spawning = void 0;
|
|
1645
|
+
throw err;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async ensureCapacity() {
|
|
1649
|
+
const liveIds = [...this.sessions.values()].filter((s) => s.proc);
|
|
1650
|
+
if (liveIds.length < config.maxLiveSessions) return;
|
|
1651
|
+
let victim = null;
|
|
1652
|
+
let oldest = Infinity;
|
|
1653
|
+
for (const s of liveIds) {
|
|
1654
|
+
if (!s.running && s.lastActivity < oldest) {
|
|
1655
|
+
oldest = s.lastActivity;
|
|
1656
|
+
victim = s;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
if (victim) {
|
|
1660
|
+
this.log.info({ sessionId: victim.sessionId }, "idle process evicted for capacity");
|
|
1661
|
+
victim.proc?.dispose();
|
|
1662
|
+
victim.proc = void 0;
|
|
1663
|
+
} else {
|
|
1664
|
+
this.log.warn("at capacity but all processes are busy");
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// -------------------------------------------------------------------------
|
|
1668
|
+
// Creating / opening
|
|
1669
|
+
// -------------------------------------------------------------------------
|
|
1670
|
+
/** Create a new session. Spawns immediately so we get a real kiro sessionId. */
|
|
1671
|
+
async createSession(opts) {
|
|
1672
|
+
const cwd = resolveCwd(opts.cwd);
|
|
1673
|
+
const tempId = `pending-${Date.now()}-${Math.floor(this.sessions.size)}`;
|
|
1674
|
+
const store = new EventStore(tempId);
|
|
1675
|
+
const s = new Session(tempId, store, cwd);
|
|
1676
|
+
s.agentId = opts.agentId ?? config.defaultAgent;
|
|
1677
|
+
s.currentModeId = s.agentId;
|
|
1678
|
+
s.modelId = opts.modelId;
|
|
1679
|
+
this.sessions.set(tempId, s);
|
|
1680
|
+
try {
|
|
1681
|
+
await this.ensureProc(s);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
this.evict(s.sessionId);
|
|
1684
|
+
throw err;
|
|
1685
|
+
}
|
|
1686
|
+
const folder = path9.basename(s.cwd);
|
|
1687
|
+
if (folder) {
|
|
1688
|
+
this.store.setTitle(s.sessionId, folder);
|
|
1689
|
+
s.title = folder;
|
|
1690
|
+
}
|
|
1691
|
+
return this.buildDetail(s, []);
|
|
1692
|
+
}
|
|
1693
|
+
// -------------------------------------------------------------------------
|
|
1694
|
+
// Actions - these spawn the process lazily.
|
|
1695
|
+
// -------------------------------------------------------------------------
|
|
1696
|
+
async runPrompt(sessionId, content) {
|
|
1697
|
+
const s = await this.ensureOpen(sessionId);
|
|
1698
|
+
const proc = await this.ensureProc(s);
|
|
1699
|
+
if (s.running) throw new Error("A turn is already running for this session");
|
|
1700
|
+
s.running = true;
|
|
1701
|
+
s.lastActivity = Date.now();
|
|
1702
|
+
s.record({ kind: "turn_started", prompt: content });
|
|
1703
|
+
proc.prompt({ sessionId: s.sessionId, prompt: content }).then((res) => s.record({ kind: "turn_ended", stopReason: res.stopReason })).catch((err) => {
|
|
1704
|
+
this.log.error({ err, sessionId: s.sessionId }, "prompt turn failed");
|
|
1705
|
+
const tail = proc.stderrTail();
|
|
1706
|
+
const message = tail && !err.message.includes(tail) ? `${err.message}
|
|
1707
|
+
|
|
1708
|
+
kiro-cli output:
|
|
1709
|
+
${tail}` : err.message;
|
|
1710
|
+
s.record({ kind: "turn_error", message });
|
|
1711
|
+
}).finally(() => {
|
|
1712
|
+
s.running = false;
|
|
1713
|
+
s.lastActivity = Date.now();
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
cancel(sessionId) {
|
|
1717
|
+
const s = this.sessions.get(sessionId);
|
|
1718
|
+
s?.proc?.cancel(s.sessionId);
|
|
1719
|
+
}
|
|
1720
|
+
async setMode(sessionId, modeId) {
|
|
1721
|
+
const s = await this.ensureOpen(sessionId);
|
|
1722
|
+
const proc = await this.ensureProc(s);
|
|
1723
|
+
await proc.setMode(s.sessionId, modeId);
|
|
1724
|
+
s.currentModeId = modeId;
|
|
1725
|
+
s.agentId = modeId;
|
|
1726
|
+
s.lastActivity = Date.now();
|
|
1727
|
+
}
|
|
1728
|
+
async setModel(sessionId, modelId) {
|
|
1729
|
+
const s = await this.ensureOpen(sessionId);
|
|
1730
|
+
const proc = await this.ensureProc(s);
|
|
1731
|
+
await proc.setModel(s.sessionId, modelId);
|
|
1732
|
+
s.modelId = modelId;
|
|
1733
|
+
s.lastActivity = Date.now();
|
|
1734
|
+
}
|
|
1735
|
+
async execCommand(sessionId, command, args) {
|
|
1736
|
+
const s = await this.ensureOpen(sessionId);
|
|
1737
|
+
const proc = await this.ensureProc(s);
|
|
1738
|
+
await proc.execCommand(s.sessionId, command, args);
|
|
1739
|
+
s.lastActivity = Date.now();
|
|
1740
|
+
}
|
|
1741
|
+
// -------------------------------------------------------------------------
|
|
1742
|
+
// Listing / detail - never spawns.
|
|
1743
|
+
// -------------------------------------------------------------------------
|
|
1744
|
+
async listSessions() {
|
|
1745
|
+
const persisted = await listPersistedSessions(this.log);
|
|
1746
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1747
|
+
for (const p of persisted) {
|
|
1748
|
+
byId.set(p.sessionId, {
|
|
1749
|
+
...p,
|
|
1750
|
+
title: this.store.getTitle(p.sessionId) ?? p.title,
|
|
1751
|
+
cwd: this.store.getCwd(p.sessionId) ?? p.cwd
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
for (const s of this.sessions.values()) {
|
|
1755
|
+
const snap = s.turnState.get();
|
|
1756
|
+
const base = byId.get(s.sessionId);
|
|
1757
|
+
byId.set(s.sessionId, {
|
|
1758
|
+
sessionId: s.sessionId,
|
|
1759
|
+
title: this.store.getTitle(s.sessionId) ?? base?.title ?? s.title,
|
|
1760
|
+
cwd: s.cwd,
|
|
1761
|
+
createdAt: base?.createdAt ?? s.createdAt,
|
|
1762
|
+
updatedAt: base?.updatedAt ?? s.updatedAt,
|
|
1763
|
+
liveness: s.proc ? "live" : "dormant",
|
|
1764
|
+
agentId: s.agentId ?? base?.agentId,
|
|
1765
|
+
modelId: s.modelId ?? base?.modelId,
|
|
1766
|
+
running: s.running,
|
|
1767
|
+
creditsSpent: snap.creditsSpent || base?.creditsSpent,
|
|
1768
|
+
contextUsagePercentage: snap.contextUsagePercentage || base?.contextUsagePercentage
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1772
|
+
}
|
|
1773
|
+
async getDetail(sessionId) {
|
|
1774
|
+
const transcript = await hydrateTranscript(sessionId);
|
|
1775
|
+
const s = this.sessions.get(sessionId);
|
|
1776
|
+
if (s) return this.buildDetail(s, transcript);
|
|
1777
|
+
const persisted = await readPersistedSession(sessionId);
|
|
1778
|
+
if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
|
|
1779
|
+
return {
|
|
1780
|
+
summary: {
|
|
1781
|
+
...persisted,
|
|
1782
|
+
title: this.store.getTitle(sessionId) ?? persisted.title,
|
|
1783
|
+
cwd: this.store.getCwd(sessionId) ?? persisted.cwd
|
|
1784
|
+
},
|
|
1785
|
+
modes: [],
|
|
1786
|
+
currentModeId: persisted.agentId,
|
|
1787
|
+
transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
|
|
1788
|
+
transcriptTotal: transcript.length,
|
|
1789
|
+
observability: {
|
|
1790
|
+
...emptyObservabilitySnapshot(),
|
|
1791
|
+
creditsSpent: persisted.creditsSpent ?? 0,
|
|
1792
|
+
contextUsagePercentage: persisted.contextUsagePercentage ?? 0
|
|
1793
|
+
},
|
|
1794
|
+
head: 0
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* A slice of the transcript for lazy "load older on scroll up". Re-hydrates
|
|
1799
|
+
* from disk (fast: ~150ms even for a multi-MB session) and returns the
|
|
1800
|
+
* requested window. offset/limit are clamped to the transcript bounds.
|
|
1801
|
+
*/
|
|
1802
|
+
async getTranscriptPage(sessionId, offset, limit) {
|
|
1803
|
+
const transcript = await hydrateTranscript(sessionId);
|
|
1804
|
+
const start = Math.max(0, Math.min(offset, transcript.length));
|
|
1805
|
+
const end = Math.max(start, Math.min(start + limit, transcript.length));
|
|
1806
|
+
return transcript.slice(start, end);
|
|
1807
|
+
}
|
|
1808
|
+
buildDetail(s, transcript) {
|
|
1809
|
+
const snap = s.turnState.get();
|
|
1810
|
+
const firstMessage = transcript.find((it) => it.type === "message");
|
|
1811
|
+
const firstText = firstMessage?.type === "message" ? firstMessage.message.text : void 0;
|
|
1812
|
+
return {
|
|
1813
|
+
summary: {
|
|
1814
|
+
sessionId: s.sessionId,
|
|
1815
|
+
title: this.store.getTitle(s.sessionId) ?? (firstText?.slice(0, 60) || s.title),
|
|
1816
|
+
cwd: s.cwd,
|
|
1817
|
+
createdAt: s.createdAt,
|
|
1818
|
+
updatedAt: s.updatedAt,
|
|
1819
|
+
liveness: s.proc ? "live" : "dormant",
|
|
1820
|
+
agentId: s.agentId,
|
|
1821
|
+
modelId: s.modelId,
|
|
1822
|
+
running: s.running,
|
|
1823
|
+
creditsSpent: snap.creditsSpent,
|
|
1824
|
+
contextUsagePercentage: snap.contextUsagePercentage
|
|
1825
|
+
},
|
|
1826
|
+
modes: s.availableModes,
|
|
1827
|
+
currentModeId: s.currentModeId,
|
|
1828
|
+
// Only the tail is sent on load; replayHead/title use the full transcript.
|
|
1829
|
+
transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
|
|
1830
|
+
transcriptTotal: transcript.length,
|
|
1831
|
+
observability: snap,
|
|
1832
|
+
head: this.replayHead(s, transcript)
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* The cursor a reconnecting client should start from. Normally the store
|
|
1837
|
+
* head, but while a turn is in flight the prompt/response are not yet in
|
|
1838
|
+
* kiro's persisted jsonl (kiro writes a turn only when it completes), so the
|
|
1839
|
+
* hydrated transcript is missing them. Rewind to just before the in-flight
|
|
1840
|
+
* turn_started so the WS replays the whole in-flight turn. Guarded against
|
|
1841
|
+
* duplication in case kiro ever persists the prompt at turn start.
|
|
1842
|
+
*/
|
|
1843
|
+
replayHead(s, transcript) {
|
|
1844
|
+
const head = s.store.head();
|
|
1845
|
+
if (!s.running) return head;
|
|
1846
|
+
const { events } = s.store.getSince(0);
|
|
1847
|
+
let started;
|
|
1848
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
1849
|
+
if (events[i].payload.kind === "turn_started") {
|
|
1850
|
+
started = events[i];
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (!started || started.payload.kind !== "turn_started") return head;
|
|
1855
|
+
const promptText = stripAttachmentsLine(
|
|
1856
|
+
started.payload.prompt.filter((b) => b.type === "text").map((b) => b.text).join("")
|
|
1857
|
+
).trim();
|
|
1858
|
+
for (let i = transcript.length - 1; i >= 0; i--) {
|
|
1859
|
+
const it = transcript[i];
|
|
1860
|
+
if (it.type === "message" && it.message.role === "user") {
|
|
1861
|
+
if (promptText && it.message.text.trim() === promptText) return head;
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return started.seq - 1;
|
|
1866
|
+
}
|
|
1867
|
+
// -------------------------------------------------------------------------
|
|
1868
|
+
// Teardown
|
|
1869
|
+
// -------------------------------------------------------------------------
|
|
1870
|
+
evict(sessionId) {
|
|
1871
|
+
const s = this.sessions.get(sessionId);
|
|
1872
|
+
if (!s) return;
|
|
1873
|
+
this.sessions.delete(sessionId);
|
|
1874
|
+
s.proc?.dispose();
|
|
1875
|
+
s.store.dispose();
|
|
1876
|
+
}
|
|
1877
|
+
// Permanently delete a session: evict it from memory, remove its on-disk
|
|
1878
|
+
// files, and drop any title override.
|
|
1879
|
+
async deleteSession(sessionId) {
|
|
1880
|
+
const s = this.sessions.get(sessionId);
|
|
1881
|
+
if (s?.proc) {
|
|
1882
|
+
await s.proc.disposeAndWait().catch(() => {
|
|
1883
|
+
});
|
|
1884
|
+
s.proc = void 0;
|
|
1885
|
+
s.running = false;
|
|
1886
|
+
}
|
|
1887
|
+
this.evict(sessionId);
|
|
1888
|
+
this.store.remove(sessionId);
|
|
1889
|
+
await deletePersistedSession(sessionId);
|
|
1890
|
+
setTimeout(() => void deletePersistedSession(sessionId).catch(() => {
|
|
1891
|
+
}), 2500).unref?.();
|
|
1892
|
+
}
|
|
1893
|
+
disposeAll() {
|
|
1894
|
+
for (const id of [...this.sessions.keys()]) this.evict(id);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
// server/src/util/rateLimit.ts
|
|
1901
|
+
var AttemptLimiter;
|
|
1902
|
+
var init_rateLimit = __esm({
|
|
1903
|
+
"server/src/util/rateLimit.ts"() {
|
|
1904
|
+
"use strict";
|
|
1905
|
+
AttemptLimiter = class {
|
|
1906
|
+
constructor(max, windowMs) {
|
|
1907
|
+
this.max = max;
|
|
1908
|
+
this.windowMs = windowMs;
|
|
1909
|
+
}
|
|
1910
|
+
hits = /* @__PURE__ */ new Map();
|
|
1911
|
+
/** Whether this key may attempt again, and how long to wait if not. */
|
|
1912
|
+
check(key, now = Date.now()) {
|
|
1913
|
+
this.prune(now);
|
|
1914
|
+
const hit = this.hits.get(key);
|
|
1915
|
+
if (!hit || hit.count < this.max) return { allowed: true };
|
|
1916
|
+
return { allowed: false, retryAfterSeconds: Math.ceil((hit.resetAt - now) / 1e3) };
|
|
1917
|
+
}
|
|
1918
|
+
/** Record a failure, starting a fresh window if the previous one has expired. */
|
|
1919
|
+
fail(key, now = Date.now()) {
|
|
1920
|
+
this.prune(now);
|
|
1921
|
+
const hit = this.hits.get(key);
|
|
1922
|
+
if (!hit) {
|
|
1923
|
+
this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
hit.count += 1;
|
|
1927
|
+
}
|
|
1928
|
+
/** Forget a key's failures, called after a successful attempt. */
|
|
1929
|
+
succeed(key) {
|
|
1930
|
+
this.hits.delete(key);
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Drop expired entries. Without this the map grows with every distinct source
|
|
1934
|
+
* address, which would turn the defence into a memory leak.
|
|
1935
|
+
*/
|
|
1936
|
+
prune(now) {
|
|
1937
|
+
for (const [key, hit] of this.hits) {
|
|
1938
|
+
if (hit.resetAt <= now) this.hits.delete(key);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// server/src/routes/auth.ts
|
|
1946
|
+
import crypto3 from "node:crypto";
|
|
1947
|
+
import fastifyCookie from "@fastify/cookie";
|
|
1948
|
+
function secretMatches(supplied, expected) {
|
|
1949
|
+
if (typeof supplied !== "string") return false;
|
|
1950
|
+
const a = crypto3.createHash("sha256").update(supplied).digest();
|
|
1951
|
+
const b = crypto3.createHash("sha256").update(expected).digest();
|
|
1952
|
+
return crypto3.timingSafeEqual(a, b);
|
|
1953
|
+
}
|
|
1954
|
+
function extractToken(req) {
|
|
1955
|
+
const auth = req.headers["authorization"];
|
|
1956
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
1957
|
+
return auth.slice("Bearer ".length).trim();
|
|
1958
|
+
}
|
|
1959
|
+
const q = req.query;
|
|
1960
|
+
if (q?.token) return q.token;
|
|
1961
|
+
return void 0;
|
|
1962
|
+
}
|
|
1963
|
+
function authDisabled() {
|
|
1964
|
+
return !config.token;
|
|
1965
|
+
}
|
|
1966
|
+
function sessionToken(req) {
|
|
1967
|
+
return req.cookies?.[SESSION_COOKIE];
|
|
1968
|
+
}
|
|
1969
|
+
function hasValidSession(req) {
|
|
1970
|
+
return logins.verify(sessionToken(req)) !== null;
|
|
1971
|
+
}
|
|
1972
|
+
function cookieOptions() {
|
|
1973
|
+
return {
|
|
1974
|
+
path: "/",
|
|
1975
|
+
httpOnly: true,
|
|
1976
|
+
sameSite: "lax",
|
|
1977
|
+
secure: "auto",
|
|
1978
|
+
maxAge: config.sessionTtlSeconds
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
async function registerAuth(app) {
|
|
1982
|
+
await app.register(fastifyCookie);
|
|
1983
|
+
app.post("/api/login", async (req, reply) => {
|
|
1984
|
+
if (!authDisabled()) {
|
|
1985
|
+
const limit = loginLimiter.check(req.ip);
|
|
1986
|
+
if (!limit.allowed) {
|
|
1987
|
+
return reply.code(429).header("retry-after", String(limit.retryAfterSeconds)).send({ error: `Too many attempts; try again in ${limit.retryAfterSeconds}s` });
|
|
1988
|
+
}
|
|
1989
|
+
const body = req.body ?? {};
|
|
1990
|
+
const supplied = body.token ?? extractToken(req);
|
|
1991
|
+
if (!secretMatches(supplied, config.token)) {
|
|
1992
|
+
loginLimiter.fail(req.ip);
|
|
1993
|
+
return reply.code(401).send({ error: "Invalid token" });
|
|
1994
|
+
}
|
|
1995
|
+
loginLimiter.succeed(req.ip);
|
|
1996
|
+
}
|
|
1997
|
+
const ua = req.headers["user-agent"];
|
|
1998
|
+
const { token } = logins.create(typeof ua === "string" ? ua : void 0);
|
|
1999
|
+
reply.setCookie(SESSION_COOKIE, token, cookieOptions());
|
|
2000
|
+
return { ok: true };
|
|
2001
|
+
});
|
|
2002
|
+
app.post("/api/logout", async (req, reply) => {
|
|
2003
|
+
logins.revokeToken(sessionToken(req));
|
|
2004
|
+
reply.clearCookie(SESSION_COOKIE, { path: "/" });
|
|
2005
|
+
return { ok: true };
|
|
2006
|
+
});
|
|
2007
|
+
app.get("/api/devices", async (req) => {
|
|
2008
|
+
return { devices: logins.list(sessionToken(req)) };
|
|
2009
|
+
});
|
|
2010
|
+
app.delete("/api/devices/:id", async (req, reply) => {
|
|
2011
|
+
const { id } = req.params;
|
|
2012
|
+
const current = logins.list(sessionToken(req)).find((d) => d.current);
|
|
2013
|
+
const removed = logins.revokeId(id);
|
|
2014
|
+
if (current?.id === id) reply.clearCookie(SESSION_COOKIE, { path: "/" });
|
|
2015
|
+
return { ok: removed };
|
|
2016
|
+
});
|
|
2017
|
+
app.post("/api/logout-all", async (_req, reply) => {
|
|
2018
|
+
logins.revokeAll();
|
|
2019
|
+
reply.clearCookie(SESSION_COOKIE, { path: "/" });
|
|
2020
|
+
return { ok: true };
|
|
2021
|
+
});
|
|
2022
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
2023
|
+
if (!req.url.startsWith("/api/")) return;
|
|
2024
|
+
if (req.url === "/api/health" || req.url === "/api/login") return;
|
|
2025
|
+
if (authDisabled()) return;
|
|
2026
|
+
if (hasValidSession(req)) return;
|
|
2027
|
+
reply.code(401).send({ error: "Unauthorized: log in first" });
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
var SESSION_COOKIE, logins, loginLimiter;
|
|
2031
|
+
var init_auth = __esm({
|
|
2032
|
+
"server/src/routes/auth.ts"() {
|
|
2033
|
+
"use strict";
|
|
2034
|
+
init_config();
|
|
2035
|
+
init_logins();
|
|
2036
|
+
init_rateLimit();
|
|
2037
|
+
SESSION_COOKIE = "casper.sid";
|
|
2038
|
+
logins = new LoginStore();
|
|
2039
|
+
loginLimiter = new AttemptLimiter(10, 15 * 60 * 1e3);
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// server/src/session/models.ts
|
|
2044
|
+
import { execFile } from "node:child_process";
|
|
2045
|
+
import { promisify } from "node:util";
|
|
2046
|
+
async function listModels(log) {
|
|
2047
|
+
if (cache) return cache;
|
|
2048
|
+
const { stdout } = await execFileAsync(
|
|
2049
|
+
config.kiroBin,
|
|
2050
|
+
["chat", "--list-models", "-f", "json"],
|
|
2051
|
+
{ maxBuffer: 4 * 1024 * 1024 }
|
|
2052
|
+
);
|
|
2053
|
+
const parsed = JSON.parse(stdout);
|
|
2054
|
+
cache = parsed.models.map((m, i) => ({
|
|
2055
|
+
modelId: m.model_id,
|
|
2056
|
+
modelName: m.model_name,
|
|
2057
|
+
description: m.description,
|
|
2058
|
+
contextWindowTokens: m.context_window_tokens,
|
|
2059
|
+
rateMultiplier: m.rate_multiplier,
|
|
2060
|
+
rateUnit: m.rate_unit,
|
|
2061
|
+
isDefault: m.default_model ?? i === 0
|
|
2062
|
+
}));
|
|
2063
|
+
log.info({ count: cache.length }, "loaded model list");
|
|
2064
|
+
return cache;
|
|
2065
|
+
}
|
|
2066
|
+
async function kiroVersion() {
|
|
2067
|
+
try {
|
|
2068
|
+
const { stdout } = await execFileAsync(config.kiroBin, ["--version"]);
|
|
2069
|
+
return stdout.trim();
|
|
2070
|
+
} catch {
|
|
2071
|
+
return void 0;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
var execFileAsync, cache;
|
|
2075
|
+
var init_models = __esm({
|
|
2076
|
+
"server/src/session/models.ts"() {
|
|
2077
|
+
"use strict";
|
|
2078
|
+
init_config();
|
|
2079
|
+
execFileAsync = promisify(execFile);
|
|
2080
|
+
cache = null;
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
// server/src/routes/models.ts
|
|
2085
|
+
function registerModelRoutes(app) {
|
|
2086
|
+
app.get("/api/models", async () => {
|
|
2087
|
+
const models = await listModels(app.log);
|
|
2088
|
+
return { models };
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
var init_models2 = __esm({
|
|
2092
|
+
"server/src/routes/models.ts"() {
|
|
2093
|
+
"use strict";
|
|
2094
|
+
init_models();
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
// server/src/session/agents.ts
|
|
2099
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2100
|
+
import { promisify as promisify2 } from "node:util";
|
|
2101
|
+
async function listAgents() {
|
|
2102
|
+
if (cache2) return cache2;
|
|
2103
|
+
const found = /* @__PURE__ */ new Map();
|
|
2104
|
+
for (const b of BUILTIN) found.set(b.id, b);
|
|
2105
|
+
try {
|
|
2106
|
+
const { stdout, stderr } = await execFileAsync2(config.kiroBin, ["agent", "list"], {
|
|
2107
|
+
cwd: config.defaultCwd,
|
|
2108
|
+
maxBuffer: 2 * 1024 * 1024
|
|
2109
|
+
});
|
|
2110
|
+
const text = (stderr || "") + "\n" + (stdout || "");
|
|
2111
|
+
for (const rawLine of text.split("\n")) {
|
|
2112
|
+
const line = rawLine.replace(ANSI, "");
|
|
2113
|
+
const m = /^\s{0,2}(\*\s)?([A-Za-z0-9_-]+)\s{2,}(Global|Workspace|Local)\b/.exec(
|
|
2114
|
+
line
|
|
2115
|
+
);
|
|
2116
|
+
if (!m) continue;
|
|
2117
|
+
const id = m[2];
|
|
2118
|
+
if (!found.has(id)) found.set(id, { id, name: id });
|
|
2119
|
+
}
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2122
|
+
cache2 = [...found.values()];
|
|
2123
|
+
return cache2;
|
|
2124
|
+
}
|
|
2125
|
+
var execFileAsync2, BUILTIN, cache2, ANSI;
|
|
2126
|
+
var init_agents = __esm({
|
|
2127
|
+
"server/src/session/agents.ts"() {
|
|
2128
|
+
"use strict";
|
|
2129
|
+
init_config();
|
|
2130
|
+
execFileAsync2 = promisify2(execFile2);
|
|
2131
|
+
BUILTIN = [
|
|
2132
|
+
{ id: "kiro_default", name: "kiro_default", description: "General-purpose Kiro agent" },
|
|
2133
|
+
{ id: "kiro_planner", name: "kiro_planner", description: "Planning-focused agent" },
|
|
2134
|
+
{ id: "kiro_guide", name: "kiro_guide", description: "Guidance and Q&A agent" }
|
|
2135
|
+
];
|
|
2136
|
+
cache2 = null;
|
|
2137
|
+
ANSI = /\x1b\[[0-9;]*m/g;
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
// server/src/routes/agents.ts
|
|
2142
|
+
function registerAgentRoutes(app) {
|
|
2143
|
+
app.get("/api/agents", async () => {
|
|
2144
|
+
return { agents: await listAgents(), defaultAgentId: config.defaultAgent };
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
var init_agents2 = __esm({
|
|
2148
|
+
"server/src/routes/agents.ts"() {
|
|
2149
|
+
"use strict";
|
|
2150
|
+
init_agents();
|
|
2151
|
+
init_config();
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
|
|
2155
|
+
// server/src/routes/fs.ts
|
|
2156
|
+
import fs9 from "node:fs/promises";
|
|
2157
|
+
import { createReadStream } from "node:fs";
|
|
2158
|
+
import path10 from "node:path";
|
|
2159
|
+
function confinedPath(input) {
|
|
2160
|
+
return confineToRoot(config.fileRoot, input);
|
|
2161
|
+
}
|
|
2162
|
+
function registerFsRoutes(app) {
|
|
2163
|
+
app.get(
|
|
2164
|
+
"/api/fs/dirs",
|
|
2165
|
+
async (req, reply) => {
|
|
2166
|
+
const input = (req.query.path ?? "").trim();
|
|
2167
|
+
const base = config.defaultCwd;
|
|
2168
|
+
const endsWithSep = input.endsWith("/");
|
|
2169
|
+
const resolved = input ? path10.resolve(base, input) : base;
|
|
2170
|
+
const dir = endsWithSep || !input ? resolved : path10.dirname(resolved);
|
|
2171
|
+
const prefix = endsWithSep || !input ? "" : path10.basename(resolved);
|
|
2172
|
+
const targetKind = await fs9.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
|
|
2173
|
+
if (confinedPath(dir) === null) {
|
|
2174
|
+
reply.code(403);
|
|
2175
|
+
return { error: "Path outside allowed root" };
|
|
2176
|
+
}
|
|
2177
|
+
const realDir = await realConfineToRoot(config.fileRoot, dir);
|
|
2178
|
+
if (!realDir) {
|
|
2179
|
+
return { dir, entries: [], target: resolved, targetKind };
|
|
2180
|
+
}
|
|
2181
|
+
let entries = [];
|
|
2182
|
+
try {
|
|
2183
|
+
const dirents = await fs9.readdir(realDir, { withFileTypes: true });
|
|
2184
|
+
const candidates = dirents.filter((d) => !d.name.startsWith("."));
|
|
2185
|
+
const checks2 = await Promise.all(
|
|
2186
|
+
candidates.map(async (d) => {
|
|
2187
|
+
if (d.isDirectory()) return d.name;
|
|
2188
|
+
if (!d.isSymbolicLink()) return null;
|
|
2189
|
+
const full = path10.join(realDir, d.name);
|
|
2190
|
+
const real = await realConfineToRoot(config.fileRoot, full);
|
|
2191
|
+
if (!real) return null;
|
|
2192
|
+
try {
|
|
2193
|
+
return (await fs9.stat(real)).isDirectory() ? d.name : null;
|
|
2194
|
+
} catch {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
})
|
|
2198
|
+
);
|
|
2199
|
+
entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => a.localeCompare(b)).slice(0, 20).map((name) => path10.join(dir, name));
|
|
2200
|
+
} catch {
|
|
2201
|
+
entries = [];
|
|
2202
|
+
}
|
|
2203
|
+
return { dir, entries, target: resolved, targetKind };
|
|
2204
|
+
}
|
|
2205
|
+
);
|
|
2206
|
+
app.get(
|
|
2207
|
+
"/api/fs/image",
|
|
2208
|
+
async (req, reply) => {
|
|
2209
|
+
const filePath = (req.query.path ?? "").trim();
|
|
2210
|
+
if (!filePath) {
|
|
2211
|
+
reply.code(400);
|
|
2212
|
+
return { error: "path parameter is required" };
|
|
2213
|
+
}
|
|
2214
|
+
if (!path10.isAbsolute(filePath)) {
|
|
2215
|
+
reply.code(400);
|
|
2216
|
+
return { error: "path must be absolute" };
|
|
2217
|
+
}
|
|
2218
|
+
const resolved = confinedPath(filePath);
|
|
2219
|
+
if (resolved === null) {
|
|
2220
|
+
reply.code(403);
|
|
2221
|
+
return { error: "Path outside allowed root" };
|
|
2222
|
+
}
|
|
2223
|
+
const ext = path10.extname(resolved).toLowerCase();
|
|
2224
|
+
const mime = IMAGE_MIMES[ext];
|
|
2225
|
+
if (!mime) {
|
|
2226
|
+
reply.code(400);
|
|
2227
|
+
return { error: `Not a supported image type: ${ext}` };
|
|
2228
|
+
}
|
|
2229
|
+
const real = await realConfineToRoot(config.fileRoot, resolved);
|
|
2230
|
+
if (!real) {
|
|
2231
|
+
reply.code(404);
|
|
2232
|
+
return { error: "File not found" };
|
|
2233
|
+
}
|
|
2234
|
+
let stat;
|
|
2235
|
+
try {
|
|
2236
|
+
stat = await fs9.stat(real);
|
|
2237
|
+
} catch {
|
|
2238
|
+
reply.code(404);
|
|
2239
|
+
return { error: "File not found" };
|
|
2240
|
+
}
|
|
2241
|
+
if (!stat.isFile()) {
|
|
2242
|
+
reply.code(400);
|
|
2243
|
+
return { error: "Path is not a file" };
|
|
2244
|
+
}
|
|
2245
|
+
if (stat.size > MAX_IMAGE_BYTES) {
|
|
2246
|
+
reply.code(413);
|
|
2247
|
+
return { error: "Image too large" };
|
|
2248
|
+
}
|
|
2249
|
+
const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
|
|
2250
|
+
reply.header("Cache-Control", "private, max-age=3600");
|
|
2251
|
+
reply.header("ETag", etag);
|
|
2252
|
+
reply.header("Last-Modified", stat.mtime.toUTCString());
|
|
2253
|
+
if (req.headers["if-none-match"] === etag) {
|
|
2254
|
+
reply.code(304);
|
|
2255
|
+
return reply.send();
|
|
2256
|
+
}
|
|
2257
|
+
reply.header("Content-Type", mime);
|
|
2258
|
+
reply.header("Content-Length", stat.size);
|
|
2259
|
+
return reply.send(createReadStream(real));
|
|
2260
|
+
}
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
var IMAGE_MIMES, MAX_IMAGE_BYTES;
|
|
2264
|
+
var init_fs = __esm({
|
|
2265
|
+
"server/src/routes/fs.ts"() {
|
|
2266
|
+
"use strict";
|
|
2267
|
+
init_config();
|
|
2268
|
+
init_paths2();
|
|
2269
|
+
IMAGE_MIMES = {
|
|
2270
|
+
".png": "image/png",
|
|
2271
|
+
".jpg": "image/jpeg",
|
|
2272
|
+
".jpeg": "image/jpeg",
|
|
2273
|
+
".gif": "image/gif",
|
|
2274
|
+
".webp": "image/webp",
|
|
2275
|
+
".svg": "image/svg+xml",
|
|
2276
|
+
".bmp": "image/bmp",
|
|
2277
|
+
".ico": "image/x-icon",
|
|
2278
|
+
".avif": "image/avif"
|
|
2279
|
+
};
|
|
2280
|
+
MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
// server/src/routes/sessions.ts
|
|
2285
|
+
function registerSessionRoutes(app, manager) {
|
|
2286
|
+
app.get("/api/sessions", async () => {
|
|
2287
|
+
return { sessions: await manager.listSessions() };
|
|
2288
|
+
});
|
|
2289
|
+
app.post("/api/sessions", async (req, reply) => {
|
|
2290
|
+
const body = req.body ?? {};
|
|
2291
|
+
try {
|
|
2292
|
+
return await manager.createSession({
|
|
2293
|
+
cwd: body.cwd,
|
|
2294
|
+
agentId: body.agentId,
|
|
2295
|
+
modelId: body.modelId
|
|
2296
|
+
});
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
reply.code(400);
|
|
2299
|
+
return { error: err.message };
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
app.get("/api/sessions/:id", async (req, reply) => {
|
|
2303
|
+
try {
|
|
2304
|
+
return await manager.getDetail(req.params.id);
|
|
2305
|
+
} catch (err) {
|
|
2306
|
+
reply.code(404);
|
|
2307
|
+
return { error: err.message };
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
app.get(
|
|
2311
|
+
"/api/sessions/:id/transcript",
|
|
2312
|
+
async (req, reply) => {
|
|
2313
|
+
const offset = Number.parseInt(req.query.offset ?? "", 10);
|
|
2314
|
+
const limit = Number.parseInt(req.query.limit ?? "", 10);
|
|
2315
|
+
if (!Number.isFinite(offset) || offset < 0 || !Number.isFinite(limit) || limit <= 0) {
|
|
2316
|
+
reply.code(400);
|
|
2317
|
+
return { error: "offset (>=0) and limit (>0) are required" };
|
|
2318
|
+
}
|
|
2319
|
+
try {
|
|
2320
|
+
return { items: await manager.getTranscriptPage(req.params.id, offset, limit) };
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
reply.code(404);
|
|
2323
|
+
return { error: err.message };
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
);
|
|
2327
|
+
app.post(
|
|
2328
|
+
"/api/sessions/:id/prompt",
|
|
2329
|
+
async (req, reply) => {
|
|
2330
|
+
try {
|
|
2331
|
+
await manager.runPrompt(req.params.id, req.body.prompt);
|
|
2332
|
+
return { ok: true };
|
|
2333
|
+
} catch (err) {
|
|
2334
|
+
reply.code(400);
|
|
2335
|
+
return { error: err.message };
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
);
|
|
2339
|
+
app.post("/api/sessions/:id/cancel", async (req) => {
|
|
2340
|
+
manager.cancel(req.params.id);
|
|
2341
|
+
return { ok: true };
|
|
2342
|
+
});
|
|
2343
|
+
app.post(
|
|
2344
|
+
"/api/sessions/:id/model",
|
|
2345
|
+
async (req) => {
|
|
2346
|
+
await manager.setModel(req.params.id, req.body.modelId);
|
|
2347
|
+
return { ok: true };
|
|
2348
|
+
}
|
|
2349
|
+
);
|
|
2350
|
+
app.post(
|
|
2351
|
+
"/api/sessions/:id/mode",
|
|
2352
|
+
async (req) => {
|
|
2353
|
+
await manager.setMode(req.params.id, req.body.modeId);
|
|
2354
|
+
return { ok: true };
|
|
2355
|
+
}
|
|
2356
|
+
);
|
|
2357
|
+
app.post(
|
|
2358
|
+
"/api/sessions/:id/rename",
|
|
2359
|
+
async (req) => {
|
|
2360
|
+
manager.renameSession(req.params.id, req.body.title);
|
|
2361
|
+
return { ok: true };
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2364
|
+
app.post(
|
|
2365
|
+
"/api/sessions/:id/cwd",
|
|
2366
|
+
async (req, reply) => {
|
|
2367
|
+
const cwd = (req.body?.cwd ?? "").trim();
|
|
2368
|
+
if (!cwd) {
|
|
2369
|
+
reply.code(400);
|
|
2370
|
+
return { error: "cwd is required" };
|
|
2371
|
+
}
|
|
2372
|
+
try {
|
|
2373
|
+
return { ok: true, cwd: await manager.setSessionCwd(req.params.id, cwd) };
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
reply.code(400);
|
|
2376
|
+
return { error: err.message };
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
);
|
|
2380
|
+
app.delete("/api/sessions/:id", async (req) => {
|
|
2381
|
+
await manager.deleteSession(req.params.id);
|
|
2382
|
+
return { ok: true };
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
var init_sessions = __esm({
|
|
2386
|
+
"server/src/routes/sessions.ts"() {
|
|
2387
|
+
"use strict";
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
|
|
2391
|
+
// server/src/util/filekind.ts
|
|
2392
|
+
import path11 from "node:path";
|
|
2393
|
+
function mimeForExt(ext) {
|
|
2394
|
+
return MIME_MAP[ext.toLowerCase()] ?? "application/octet-stream";
|
|
2395
|
+
}
|
|
2396
|
+
function looksBinary(buf) {
|
|
2397
|
+
if (buf.length === 0) return false;
|
|
2398
|
+
if (buf.length >= 2) {
|
|
2399
|
+
const b0 = buf[0];
|
|
2400
|
+
const b1 = buf[1];
|
|
2401
|
+
if (b0 === 255 && b1 === 254 || b0 === 254 && b1 === 255) return true;
|
|
2402
|
+
}
|
|
2403
|
+
let control = 0;
|
|
2404
|
+
for (let i = 0; i < buf.length; i++) {
|
|
2405
|
+
const b = buf[i];
|
|
2406
|
+
if (b === 0) return true;
|
|
2407
|
+
if (b < 32 && b !== 9 && b !== 10 && b !== 12 && b !== 13) control++;
|
|
2408
|
+
}
|
|
2409
|
+
return control / buf.length > 0.3;
|
|
2410
|
+
}
|
|
2411
|
+
function classifyKind(name) {
|
|
2412
|
+
const ext = path11.extname(name).toLowerCase();
|
|
2413
|
+
if (ext in IMAGE_EXTS2) return "image";
|
|
2414
|
+
if (TEXT_EXTS.has(ext)) return "text";
|
|
2415
|
+
return "binary";
|
|
2416
|
+
}
|
|
2417
|
+
var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
|
|
2418
|
+
var init_filekind = __esm({
|
|
2419
|
+
"server/src/util/filekind.ts"() {
|
|
2420
|
+
"use strict";
|
|
2421
|
+
IMAGE_EXTS2 = {
|
|
2422
|
+
".png": "image/png",
|
|
2423
|
+
".jpg": "image/jpeg",
|
|
2424
|
+
".jpeg": "image/jpeg",
|
|
2425
|
+
".gif": "image/gif",
|
|
2426
|
+
".webp": "image/webp",
|
|
2427
|
+
".svg": "image/svg+xml",
|
|
2428
|
+
".bmp": "image/bmp",
|
|
2429
|
+
".ico": "image/x-icon",
|
|
2430
|
+
".avif": "image/avif"
|
|
2431
|
+
};
|
|
2432
|
+
TEXT_EXTS = /* @__PURE__ */ new Set([
|
|
2433
|
+
".txt",
|
|
2434
|
+
".md",
|
|
2435
|
+
".markdown",
|
|
2436
|
+
".rst",
|
|
2437
|
+
".log",
|
|
2438
|
+
".csv",
|
|
2439
|
+
".tsv",
|
|
2440
|
+
".json",
|
|
2441
|
+
".jsonl",
|
|
2442
|
+
".yaml",
|
|
2443
|
+
".yml",
|
|
2444
|
+
".toml",
|
|
2445
|
+
".ini",
|
|
2446
|
+
".env",
|
|
2447
|
+
".cfg",
|
|
2448
|
+
".conf",
|
|
2449
|
+
".xml",
|
|
2450
|
+
".html",
|
|
2451
|
+
".htm",
|
|
2452
|
+
".css",
|
|
2453
|
+
".scss",
|
|
2454
|
+
".less",
|
|
2455
|
+
".js",
|
|
2456
|
+
".mjs",
|
|
2457
|
+
".cjs",
|
|
2458
|
+
".jsx",
|
|
2459
|
+
".ts",
|
|
2460
|
+
".tsx",
|
|
2461
|
+
".vue",
|
|
2462
|
+
".svelte",
|
|
2463
|
+
".py",
|
|
2464
|
+
".rb",
|
|
2465
|
+
".go",
|
|
2466
|
+
".rs",
|
|
2467
|
+
".java",
|
|
2468
|
+
".kt",
|
|
2469
|
+
".c",
|
|
2470
|
+
".h",
|
|
2471
|
+
".cpp",
|
|
2472
|
+
".hpp",
|
|
2473
|
+
".cc",
|
|
2474
|
+
".cs",
|
|
2475
|
+
".php",
|
|
2476
|
+
".swift",
|
|
2477
|
+
".scala",
|
|
2478
|
+
".sh",
|
|
2479
|
+
".bash",
|
|
2480
|
+
".zsh",
|
|
2481
|
+
".fish",
|
|
2482
|
+
".sql",
|
|
2483
|
+
".graphql",
|
|
2484
|
+
".proto",
|
|
2485
|
+
".dockerfile",
|
|
2486
|
+
".makefile",
|
|
2487
|
+
".gradle",
|
|
2488
|
+
".diff",
|
|
2489
|
+
".patch",
|
|
2490
|
+
".tex"
|
|
2491
|
+
]);
|
|
2492
|
+
MIME_MAP = {
|
|
2493
|
+
...IMAGE_EXTS2,
|
|
2494
|
+
".txt": "text/plain",
|
|
2495
|
+
".md": "text/markdown",
|
|
2496
|
+
".csv": "text/csv",
|
|
2497
|
+
".json": "application/json",
|
|
2498
|
+
".xml": "application/xml",
|
|
2499
|
+
".yaml": "text/yaml",
|
|
2500
|
+
".yml": "text/yaml",
|
|
2501
|
+
".html": "text/html",
|
|
2502
|
+
".css": "text/css",
|
|
2503
|
+
".js": "application/javascript",
|
|
2504
|
+
".mjs": "application/javascript",
|
|
2505
|
+
".ts": "text/typescript",
|
|
2506
|
+
".tsx": "text/typescript",
|
|
2507
|
+
".toml": "text/plain",
|
|
2508
|
+
".pdf": "application/pdf",
|
|
2509
|
+
".zip": "application/zip",
|
|
2510
|
+
".tar": "application/x-tar",
|
|
2511
|
+
".gz": "application/gzip",
|
|
2512
|
+
".wasm": "application/wasm",
|
|
2513
|
+
".exe": "application/vnd.microsoft.portable-executable",
|
|
2514
|
+
".dll": "application/vnd.microsoft.portable-executable",
|
|
2515
|
+
".so": "application/x-sharedlib",
|
|
2516
|
+
".bin": "application/octet-stream"
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2521
|
+
// server/src/routes/workspace.ts
|
|
2522
|
+
import fs10 from "node:fs/promises";
|
|
2523
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
2524
|
+
import path12 from "node:path";
|
|
2525
|
+
function tooLargeForPreview(size, maxBytes) {
|
|
2526
|
+
return {
|
|
2527
|
+
error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
function hexdump(buf) {
|
|
2531
|
+
const lines = [];
|
|
2532
|
+
for (let off = 0; off < buf.length; off += 16) {
|
|
2533
|
+
const slice = buf.subarray(off, off + 16);
|
|
2534
|
+
const hex = [];
|
|
2535
|
+
let ascii = "";
|
|
2536
|
+
for (let i = 0; i < 16; i++) {
|
|
2537
|
+
if (i < slice.length) {
|
|
2538
|
+
const b = slice[i];
|
|
2539
|
+
hex.push(b.toString(16).padStart(2, "0"));
|
|
2540
|
+
ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
|
|
2541
|
+
} else {
|
|
2542
|
+
hex.push(" ");
|
|
2543
|
+
}
|
|
2544
|
+
if (i === 7) hex.push("");
|
|
2545
|
+
}
|
|
2546
|
+
lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
|
|
2547
|
+
}
|
|
2548
|
+
return lines.join("\n");
|
|
2549
|
+
}
|
|
2550
|
+
function registerWorkspaceRoutes(app, manager) {
|
|
2551
|
+
app.get(
|
|
2552
|
+
"/api/sessions/:id/tree",
|
|
2553
|
+
async (req, reply) => {
|
|
2554
|
+
let cwd;
|
|
2555
|
+
try {
|
|
2556
|
+
cwd = await manager.getSessionCwd(req.params.id);
|
|
2557
|
+
} catch {
|
|
2558
|
+
reply.code(404);
|
|
2559
|
+
return { error: "Session not found" };
|
|
2560
|
+
}
|
|
2561
|
+
const relative = (req.query.path ?? "").replace(/^\/+/, "");
|
|
2562
|
+
const target = confineToRoot(cwd, relative);
|
|
2563
|
+
if (!target) {
|
|
2564
|
+
reply.code(400);
|
|
2565
|
+
return { error: "Invalid path" };
|
|
2566
|
+
}
|
|
2567
|
+
const workspaceMissing = async () => {
|
|
2568
|
+
try {
|
|
2569
|
+
return !(await fs10.stat(cwd)).isDirectory();
|
|
2570
|
+
} catch {
|
|
2571
|
+
return true;
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const notFound = async () => {
|
|
2575
|
+
reply.code(404);
|
|
2576
|
+
return await workspaceMissing() ? { error: `Workspace folder no longer exists: ${cwd}` } : { error: "Directory not found" };
|
|
2577
|
+
};
|
|
2578
|
+
const realTarget = await realConfineToRoot(config.fileRoot, target);
|
|
2579
|
+
if (!realTarget) {
|
|
2580
|
+
return notFound();
|
|
2581
|
+
}
|
|
2582
|
+
let dirents;
|
|
2583
|
+
try {
|
|
2584
|
+
dirents = await fs10.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
|
|
2585
|
+
} catch {
|
|
2586
|
+
return notFound();
|
|
2587
|
+
}
|
|
2588
|
+
const entries = [];
|
|
2589
|
+
for (const d of dirents) {
|
|
2590
|
+
const name = d.name;
|
|
2591
|
+
const entryRelative = relative ? `${relative}/${name}` : name;
|
|
2592
|
+
const entryAbsolute = path12.join(realTarget, name);
|
|
2593
|
+
let kind = d.isDirectory() ? "directory" : d.isFile() ? "file" : null;
|
|
2594
|
+
let statTarget = entryAbsolute;
|
|
2595
|
+
if (kind === null && d.isSymbolicLink()) {
|
|
2596
|
+
const real = await realConfineToRoot(config.fileRoot, entryAbsolute);
|
|
2597
|
+
if (!real) continue;
|
|
2598
|
+
try {
|
|
2599
|
+
const st = await fs10.stat(real);
|
|
2600
|
+
kind = st.isDirectory() ? "directory" : st.isFile() ? "file" : null;
|
|
2601
|
+
statTarget = real;
|
|
2602
|
+
} catch {
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
if (kind === null) continue;
|
|
2607
|
+
if (kind === "directory") {
|
|
2608
|
+
entries.push({ name, path: entryRelative, type: "directory" });
|
|
2609
|
+
} else {
|
|
2610
|
+
try {
|
|
2611
|
+
const stat = await fs10.stat(statTarget);
|
|
2612
|
+
entries.push({
|
|
2613
|
+
name,
|
|
2614
|
+
path: entryRelative,
|
|
2615
|
+
type: "file",
|
|
2616
|
+
size: stat.size,
|
|
2617
|
+
modifiedAt: stat.mtime.toISOString()
|
|
2618
|
+
});
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
entries.sort((a, b) => {
|
|
2624
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
2625
|
+
return a.name.localeCompare(b.name);
|
|
2626
|
+
});
|
|
2627
|
+
const response = { cwd, relativeTo: relative, entries };
|
|
2628
|
+
return response;
|
|
2629
|
+
}
|
|
2630
|
+
);
|
|
2631
|
+
app.get(
|
|
2632
|
+
"/api/sessions/:id/download",
|
|
2633
|
+
async (req, reply) => {
|
|
2634
|
+
let cwd;
|
|
2635
|
+
try {
|
|
2636
|
+
cwd = await manager.getSessionCwd(req.params.id);
|
|
2637
|
+
} catch {
|
|
2638
|
+
reply.code(404);
|
|
2639
|
+
return { error: "Session not found" };
|
|
2640
|
+
}
|
|
2641
|
+
const relative = (req.query.path ?? "").replace(/^\/+/, "");
|
|
2642
|
+
if (!relative) {
|
|
2643
|
+
reply.code(400);
|
|
2644
|
+
return { error: "path parameter is required" };
|
|
2645
|
+
}
|
|
2646
|
+
const target = confineToRoot(cwd, relative);
|
|
2647
|
+
if (!target) {
|
|
2648
|
+
reply.code(400);
|
|
2649
|
+
return { error: "Invalid path" };
|
|
2650
|
+
}
|
|
2651
|
+
const realTarget = await realConfineToRoot(config.fileRoot, target);
|
|
2652
|
+
if (!realTarget) {
|
|
2653
|
+
reply.code(404);
|
|
2654
|
+
return { error: "File not found" };
|
|
2655
|
+
}
|
|
2656
|
+
let stat;
|
|
2657
|
+
try {
|
|
2658
|
+
stat = await fs10.stat(realTarget);
|
|
2659
|
+
} catch {
|
|
2660
|
+
reply.code(404);
|
|
2661
|
+
return { error: "File not found" };
|
|
2662
|
+
}
|
|
2663
|
+
if (!stat.isFile()) {
|
|
2664
|
+
reply.code(400);
|
|
2665
|
+
return { error: "Path is not a file" };
|
|
2666
|
+
}
|
|
2667
|
+
if (stat.size > MAX_DOWNLOAD_BYTES) {
|
|
2668
|
+
reply.code(413);
|
|
2669
|
+
return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
|
|
2670
|
+
}
|
|
2671
|
+
const ext = path12.extname(realTarget);
|
|
2672
|
+
const filename = path12.basename(realTarget);
|
|
2673
|
+
const asciiName = filename.replace(/[^\w.\-]/g, "_");
|
|
2674
|
+
reply.header("Content-Type", mimeForExt(ext));
|
|
2675
|
+
reply.header(
|
|
2676
|
+
"Content-Disposition",
|
|
2677
|
+
`attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
2678
|
+
);
|
|
2679
|
+
reply.header("Content-Length", stat.size);
|
|
2680
|
+
return reply.send(createReadStream2(realTarget));
|
|
2681
|
+
}
|
|
2682
|
+
);
|
|
2683
|
+
app.get(
|
|
2684
|
+
"/api/sessions/:id/preview",
|
|
2685
|
+
async (req, reply) => {
|
|
2686
|
+
let cwd;
|
|
2687
|
+
try {
|
|
2688
|
+
cwd = await manager.getSessionCwd(req.params.id);
|
|
2689
|
+
} catch {
|
|
2690
|
+
reply.code(404);
|
|
2691
|
+
return { error: "Session not found" };
|
|
2692
|
+
}
|
|
2693
|
+
const relative = (req.query.path ?? "").replace(/^\/+/, "");
|
|
2694
|
+
if (!relative) {
|
|
2695
|
+
reply.code(400);
|
|
2696
|
+
return { error: "path parameter is required" };
|
|
2697
|
+
}
|
|
2698
|
+
const target = confineToRoot(cwd, relative);
|
|
2699
|
+
if (!target) {
|
|
2700
|
+
reply.code(400);
|
|
2701
|
+
return { error: "Invalid path" };
|
|
2702
|
+
}
|
|
2703
|
+
const realTarget = await realConfineToRoot(config.fileRoot, target);
|
|
2704
|
+
if (!realTarget) {
|
|
2705
|
+
reply.code(404);
|
|
2706
|
+
return { error: "File not found" };
|
|
2707
|
+
}
|
|
2708
|
+
let stat;
|
|
2709
|
+
try {
|
|
2710
|
+
stat = await fs10.stat(realTarget);
|
|
2711
|
+
} catch {
|
|
2712
|
+
reply.code(404);
|
|
2713
|
+
return { error: "File not found" };
|
|
2714
|
+
}
|
|
2715
|
+
if (!stat.isFile()) {
|
|
2716
|
+
reply.code(400);
|
|
2717
|
+
return { error: "Path is not a file" };
|
|
2718
|
+
}
|
|
2719
|
+
const ext = path12.extname(realTarget).toLowerCase();
|
|
2720
|
+
const mime = mimeForExt(ext);
|
|
2721
|
+
const isImage = mime.startsWith("image/");
|
|
2722
|
+
const isPdf = mime === "application/pdf";
|
|
2723
|
+
const kind = classifyKind(realTarget);
|
|
2724
|
+
if (isImage || isPdf) {
|
|
2725
|
+
if (stat.size > MAX_IMAGE_PREVIEW_BYTES) {
|
|
2726
|
+
reply.code(413);
|
|
2727
|
+
return tooLargeForPreview(stat.size, MAX_IMAGE_PREVIEW_BYTES);
|
|
2728
|
+
}
|
|
2729
|
+
} else if (kind !== "binary") {
|
|
2730
|
+
if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
|
|
2731
|
+
reply.code(413);
|
|
2732
|
+
return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (isImage || isPdf) {
|
|
2736
|
+
const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
|
|
2737
|
+
reply.header("Cache-Control", "private, max-age=3600");
|
|
2738
|
+
reply.header("ETag", etag);
|
|
2739
|
+
reply.header("Last-Modified", stat.mtime.toUTCString());
|
|
2740
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
2741
|
+
if (ifNoneMatch === etag) {
|
|
2742
|
+
reply.code(304);
|
|
2743
|
+
return reply.send();
|
|
2744
|
+
}
|
|
2745
|
+
reply.header("Content-Type", mime);
|
|
2746
|
+
reply.header("Content-Disposition", "inline");
|
|
2747
|
+
reply.header("Content-Length", stat.size);
|
|
2748
|
+
return reply.send(createReadStream2(realTarget));
|
|
2749
|
+
}
|
|
2750
|
+
if (kind === "binary") {
|
|
2751
|
+
let fh;
|
|
2752
|
+
try {
|
|
2753
|
+
fh = await fs10.open(realTarget, "r");
|
|
2754
|
+
const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
|
|
2755
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
2756
|
+
const head = buf.subarray(0, bytesRead);
|
|
2757
|
+
if (!looksBinary(head)) {
|
|
2758
|
+
if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
|
|
2759
|
+
reply.code(413);
|
|
2760
|
+
return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
|
|
2761
|
+
}
|
|
2762
|
+
reply.header("Content-Type", "text/plain; charset=utf-8");
|
|
2763
|
+
reply.header("Content-Disposition", "inline");
|
|
2764
|
+
reply.header("Content-Length", stat.size);
|
|
2765
|
+
return reply.send(createReadStream2(realTarget));
|
|
2766
|
+
}
|
|
2767
|
+
const dump = hexdump(head);
|
|
2768
|
+
const header = `Binary file - ${stat.size} bytes
|
|
2769
|
+
Showing first ${bytesRead} bytes as hexdump:
|
|
2770
|
+
|
|
2771
|
+
`;
|
|
2772
|
+
const body = header + dump + (stat.size > bytesRead ? "\n\n\u2026 (truncated)" : "");
|
|
2773
|
+
reply.header("Content-Type", "text/plain; charset=utf-8");
|
|
2774
|
+
reply.header("Content-Disposition", "inline");
|
|
2775
|
+
return reply.send(body);
|
|
2776
|
+
} finally {
|
|
2777
|
+
await fh?.close();
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
reply.header("Content-Type", "text/plain; charset=utf-8");
|
|
2781
|
+
reply.header("Content-Disposition", "inline");
|
|
2782
|
+
reply.header("Content-Length", stat.size);
|
|
2783
|
+
return reply.send(createReadStream2(realTarget));
|
|
2784
|
+
}
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
var MAX_DOWNLOAD_BYTES, HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
|
|
2788
|
+
var init_workspace = __esm({
|
|
2789
|
+
"server/src/routes/workspace.ts"() {
|
|
2790
|
+
"use strict";
|
|
2791
|
+
init_config();
|
|
2792
|
+
init_paths2();
|
|
2793
|
+
init_filekind();
|
|
2794
|
+
MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
2795
|
+
HEXDUMP_BYTES = 4096;
|
|
2796
|
+
MAX_TEXT_PREVIEW_BYTES = 1024 * 1024;
|
|
2797
|
+
MAX_IMAGE_PREVIEW_BYTES = 20 * 1024 * 1024;
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
// server/src/routes/uploads.ts
|
|
2802
|
+
import { createReadStream as createReadStream3, createWriteStream } from "node:fs";
|
|
2803
|
+
import fs11 from "node:fs/promises";
|
|
2804
|
+
import path13 from "node:path";
|
|
2805
|
+
import crypto4 from "node:crypto";
|
|
2806
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2807
|
+
import { pipeline } from "node:stream/promises";
|
|
2808
|
+
function sanitizeName(raw) {
|
|
2809
|
+
const base = path13.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
|
|
2810
|
+
if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
|
|
2811
|
+
return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
|
|
2812
|
+
}
|
|
2813
|
+
async function uniquePath(dir, name) {
|
|
2814
|
+
const ext = path13.extname(name);
|
|
2815
|
+
const stem = name.slice(0, name.length - ext.length);
|
|
2816
|
+
let candidate = name;
|
|
2817
|
+
let n = 1;
|
|
2818
|
+
while (true) {
|
|
2819
|
+
try {
|
|
2820
|
+
await fs11.access(path13.join(dir, candidate));
|
|
2821
|
+
candidate = `${stem} (${n++})${ext}`;
|
|
2822
|
+
} catch {
|
|
2823
|
+
return path13.join(dir, candidate);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
function sha256File(absPath) {
|
|
2828
|
+
return new Promise((resolve, reject) => {
|
|
2829
|
+
const hash = crypto4.createHash("sha256");
|
|
2830
|
+
createReadStream3(absPath).on("data", (d) => hash.update(d)).on("end", () => resolve(hash.digest("hex"))).on("error", reject);
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
function fileType(absPath) {
|
|
2834
|
+
return new Promise((resolve) => {
|
|
2835
|
+
execFile3("file", ["-b", absPath], { timeout: 5e3 }, (err, stdout) => {
|
|
2836
|
+
resolve(err ? "" : stdout.trim());
|
|
2837
|
+
});
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
async function sampleStrings(absPath, limit = 40) {
|
|
2841
|
+
let fh;
|
|
2842
|
+
try {
|
|
2843
|
+
fh = await fs11.open(absPath, "r");
|
|
2844
|
+
const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
|
|
2845
|
+
const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
|
|
2846
|
+
const out = [];
|
|
2847
|
+
let cur = "";
|
|
2848
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
2849
|
+
const c = buf[i];
|
|
2850
|
+
if (c >= 32 && c <= 126) {
|
|
2851
|
+
cur += String.fromCharCode(c);
|
|
2852
|
+
} else {
|
|
2853
|
+
if (cur.length >= 6) out.push(cur);
|
|
2854
|
+
cur = "";
|
|
2855
|
+
if (out.length >= limit) break;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (cur.length >= 6 && out.length < limit) out.push(cur);
|
|
2859
|
+
return out;
|
|
2860
|
+
} catch {
|
|
2861
|
+
return [];
|
|
2862
|
+
} finally {
|
|
2863
|
+
await fh?.close();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
function registerUploadRoutes(app, manager) {
|
|
2867
|
+
app.post(
|
|
2868
|
+
"/api/sessions/:id/uploads",
|
|
2869
|
+
async (req, reply) => {
|
|
2870
|
+
let cwd;
|
|
2871
|
+
try {
|
|
2872
|
+
cwd = await manager.getSessionCwd(req.params.id);
|
|
2873
|
+
} catch {
|
|
2874
|
+
reply.code(404);
|
|
2875
|
+
return { error: "Session not found" };
|
|
2876
|
+
}
|
|
2877
|
+
const uploadDir = path13.join(cwd, UPLOAD_SUBDIR);
|
|
2878
|
+
await fs11.mkdir(uploadDir, { recursive: true });
|
|
2879
|
+
const results = [];
|
|
2880
|
+
const parts = req.files();
|
|
2881
|
+
for await (const part of parts) {
|
|
2882
|
+
const name = sanitizeName(part.filename ?? "upload");
|
|
2883
|
+
if (confineToRoot(uploadDir, name) === null) continue;
|
|
2884
|
+
const dest = await uniquePath(uploadDir, name);
|
|
2885
|
+
try {
|
|
2886
|
+
await pipeline(part.file, createWriteStream(dest));
|
|
2887
|
+
} catch {
|
|
2888
|
+
reply.code(500);
|
|
2889
|
+
return { error: `Failed to store ${name}` };
|
|
2890
|
+
}
|
|
2891
|
+
if (part.file.truncated) {
|
|
2892
|
+
await fs11.rm(dest, { force: true });
|
|
2893
|
+
reply.code(413);
|
|
2894
|
+
return {
|
|
2895
|
+
error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
const stat = await fs11.stat(dest);
|
|
2899
|
+
const ext = path13.extname(dest);
|
|
2900
|
+
const kind = classifyKind(dest);
|
|
2901
|
+
const relative = path13.relative(cwd, dest);
|
|
2902
|
+
const uploaded = {
|
|
2903
|
+
name: path13.basename(dest),
|
|
2904
|
+
path: relative,
|
|
2905
|
+
size: stat.size,
|
|
2906
|
+
mimeType: mimeForExt(ext),
|
|
2907
|
+
kind
|
|
2908
|
+
};
|
|
2909
|
+
if (kind === "binary") {
|
|
2910
|
+
const [type, sha2563, strings] = await Promise.all([
|
|
2911
|
+
fileType(dest),
|
|
2912
|
+
sha256File(dest),
|
|
2913
|
+
sampleStrings(dest)
|
|
2914
|
+
]);
|
|
2915
|
+
uploaded.triage = {
|
|
2916
|
+
fileType: type || void 0,
|
|
2917
|
+
sha256: sha2563,
|
|
2918
|
+
strings: strings.length ? strings : void 0
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
results.push(uploaded);
|
|
2922
|
+
}
|
|
2923
|
+
const response = { files: results };
|
|
2924
|
+
return response;
|
|
2925
|
+
}
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
var UPLOAD_SUBDIR, STRINGS_SCAN_BYTES;
|
|
2929
|
+
var init_uploads = __esm({
|
|
2930
|
+
"server/src/routes/uploads.ts"() {
|
|
2931
|
+
"use strict";
|
|
2932
|
+
init_config();
|
|
2933
|
+
init_filekind();
|
|
2934
|
+
init_paths2();
|
|
2935
|
+
UPLOAD_SUBDIR = path13.join(".casper", "uploads");
|
|
2936
|
+
STRINGS_SCAN_BYTES = 256 * 1024;
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2940
|
+
// server/src/routes/health.ts
|
|
2941
|
+
function registerHealthRoute(app, manager, startedAt) {
|
|
2942
|
+
app.get("/api/health", async () => {
|
|
2943
|
+
return {
|
|
2944
|
+
status: "ok",
|
|
2945
|
+
kiroVersion: await kiroVersion(),
|
|
2946
|
+
liveSessions: manager.liveCount,
|
|
2947
|
+
uptimeMs: Date.now() - startedAt
|
|
2948
|
+
};
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
var init_health = __esm({
|
|
2952
|
+
"server/src/routes/health.ts"() {
|
|
2953
|
+
"use strict";
|
|
2954
|
+
init_models();
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
// server/src/ws/dispatch.ts
|
|
2959
|
+
function send(socket, msg) {
|
|
2960
|
+
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
|
|
2961
|
+
}
|
|
2962
|
+
async function handleClientMessage(socket, manager, sessionId, msg) {
|
|
2963
|
+
switch (msg.type) {
|
|
2964
|
+
case "ping":
|
|
2965
|
+
send(socket, { type: "pong" });
|
|
2966
|
+
return;
|
|
2967
|
+
case "hello":
|
|
2968
|
+
return;
|
|
2969
|
+
case "prompt":
|
|
2970
|
+
try {
|
|
2971
|
+
await manager.runPrompt(sessionId, msg.content);
|
|
2972
|
+
send(socket, { type: "ack", action: "prompt", ok: true });
|
|
2973
|
+
} catch (err) {
|
|
2974
|
+
send(socket, {
|
|
2975
|
+
type: "ack",
|
|
2976
|
+
action: "prompt",
|
|
2977
|
+
ok: false,
|
|
2978
|
+
error: err.message
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
return;
|
|
2982
|
+
case "cancel":
|
|
2983
|
+
try {
|
|
2984
|
+
manager.cancel(sessionId);
|
|
2985
|
+
send(socket, { type: "ack", action: "cancel", ok: true });
|
|
2986
|
+
} catch (err) {
|
|
2987
|
+
send(socket, {
|
|
2988
|
+
type: "ack",
|
|
2989
|
+
action: "cancel",
|
|
2990
|
+
ok: false,
|
|
2991
|
+
error: err.message
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
return;
|
|
2995
|
+
case "set_mode":
|
|
2996
|
+
try {
|
|
2997
|
+
await manager.setMode(sessionId, msg.modeId);
|
|
2998
|
+
send(socket, { type: "ack", action: "set_mode", ok: true });
|
|
2999
|
+
} catch (err) {
|
|
3000
|
+
send(socket, {
|
|
3001
|
+
type: "ack",
|
|
3002
|
+
action: "set_mode",
|
|
3003
|
+
ok: false,
|
|
3004
|
+
error: err.message
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
return;
|
|
3008
|
+
case "set_model":
|
|
3009
|
+
try {
|
|
3010
|
+
await manager.setModel(sessionId, msg.modelId);
|
|
3011
|
+
send(socket, { type: "ack", action: "set_model", ok: true });
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
send(socket, {
|
|
3014
|
+
type: "ack",
|
|
3015
|
+
action: "set_model",
|
|
3016
|
+
ok: false,
|
|
3017
|
+
error: err.message
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
return;
|
|
3021
|
+
case "exec_command":
|
|
3022
|
+
try {
|
|
3023
|
+
await manager.execCommand(sessionId, msg.command, msg.args);
|
|
3024
|
+
send(socket, { type: "ack", action: "exec_command", ok: true });
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
send(socket, {
|
|
3027
|
+
type: "ack",
|
|
3028
|
+
action: "exec_command",
|
|
3029
|
+
ok: false,
|
|
3030
|
+
error: err.message
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
return;
|
|
3034
|
+
default:
|
|
3035
|
+
send(socket, { type: "error", message: `Unknown message type` });
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
var init_dispatch = __esm({
|
|
3039
|
+
"server/src/ws/dispatch.ts"() {
|
|
3040
|
+
"use strict";
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
|
|
3044
|
+
// server/src/ws/gateway.ts
|
|
3045
|
+
function registerWsGateway(app, manager) {
|
|
3046
|
+
app.get("/ws", { websocket: true }, (socket, req) => {
|
|
3047
|
+
const query = req.query;
|
|
3048
|
+
if (!authDisabled() && !hasValidSession(req)) {
|
|
3049
|
+
send(socket, { type: "error", message: "Unauthorized" });
|
|
3050
|
+
socket.close(1008, "Unauthorized");
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const sessionId = query.sessionId;
|
|
3054
|
+
if (!sessionId) {
|
|
3055
|
+
send(socket, { type: "error", message: "Missing sessionId" });
|
|
3056
|
+
socket.close(1008, "Missing sessionId");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
let cursor = Number.parseInt(query.cursor ?? "0", 10) || 0;
|
|
3060
|
+
let unsubscribe = null;
|
|
3061
|
+
let alive = true;
|
|
3062
|
+
let ready = false;
|
|
3063
|
+
const forward = (event) => {
|
|
3064
|
+
if (event.seq <= cursor) return;
|
|
3065
|
+
cursor = event.seq;
|
|
3066
|
+
send(socket, { type: "event", event });
|
|
3067
|
+
};
|
|
3068
|
+
const attach = async () => {
|
|
3069
|
+
try {
|
|
3070
|
+
await manager.ensureOpen(sessionId);
|
|
3071
|
+
} catch (err) {
|
|
3072
|
+
send(socket, { type: "error", message: err.message });
|
|
3073
|
+
socket.close(1011, "open failed");
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
const store = manager.getStore(sessionId);
|
|
3077
|
+
if (!store) {
|
|
3078
|
+
send(socket, { type: "error", message: "Session store unavailable" });
|
|
3079
|
+
socket.close(1011, "no store");
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
const { events, gap } = store.getSince(cursor);
|
|
3083
|
+
if (gap) {
|
|
3084
|
+
send(socket, {
|
|
3085
|
+
type: "resync",
|
|
3086
|
+
reason: "cursor older than buffer; refetch full transcript"
|
|
3087
|
+
});
|
|
3088
|
+
cursor = store.head();
|
|
3089
|
+
} else {
|
|
3090
|
+
for (const e of events) forward(e);
|
|
3091
|
+
}
|
|
3092
|
+
send(socket, { type: "replay_complete", head: store.head() });
|
|
3093
|
+
unsubscribe = manager.onEvent(sessionId, forward);
|
|
3094
|
+
ready = true;
|
|
3095
|
+
};
|
|
3096
|
+
void attach();
|
|
3097
|
+
const heartbeat = setInterval(() => {
|
|
3098
|
+
if (!alive) {
|
|
3099
|
+
socket.terminate();
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
alive = false;
|
|
3103
|
+
try {
|
|
3104
|
+
socket.ping();
|
|
3105
|
+
} catch {
|
|
3106
|
+
}
|
|
3107
|
+
}, HEARTBEAT_MS);
|
|
3108
|
+
heartbeat.unref?.();
|
|
3109
|
+
socket.on("pong", () => {
|
|
3110
|
+
alive = true;
|
|
3111
|
+
});
|
|
3112
|
+
socket.on("message", (raw) => {
|
|
3113
|
+
alive = true;
|
|
3114
|
+
if (!ready) return;
|
|
3115
|
+
let msg;
|
|
3116
|
+
try {
|
|
3117
|
+
msg = JSON.parse(raw.toString());
|
|
3118
|
+
} catch {
|
|
3119
|
+
send(socket, { type: "error", message: "Invalid JSON" });
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
void handleClientMessage(socket, manager, sessionId, msg);
|
|
3123
|
+
});
|
|
3124
|
+
socket.on("close", () => {
|
|
3125
|
+
clearInterval(heartbeat);
|
|
3126
|
+
unsubscribe?.();
|
|
3127
|
+
});
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
var HEARTBEAT_MS;
|
|
3131
|
+
var init_gateway = __esm({
|
|
3132
|
+
"server/src/ws/gateway.ts"() {
|
|
3133
|
+
"use strict";
|
|
3134
|
+
init_auth();
|
|
3135
|
+
init_dispatch();
|
|
3136
|
+
HEARTBEAT_MS = 2e4;
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
|
|
3140
|
+
// server/src/app.ts
|
|
3141
|
+
import fs12 from "node:fs";
|
|
3142
|
+
import Fastify from "fastify";
|
|
3143
|
+
import cors from "@fastify/cors";
|
|
3144
|
+
import websocket from "@fastify/websocket";
|
|
3145
|
+
import fastifyStatic from "@fastify/static";
|
|
3146
|
+
import multipart from "@fastify/multipart";
|
|
3147
|
+
async function buildApp() {
|
|
3148
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
3149
|
+
const app = Fastify({
|
|
3150
|
+
// Honor X-Forwarded-Proto so `secure: 'auto'` cookies detect HTTPS when the
|
|
3151
|
+
// server sits behind a TLS-terminating tunnel or reverse proxy.
|
|
3152
|
+
trustProxy: true,
|
|
3153
|
+
// In production, skip the two-lines-per-request access log; our own
|
|
3154
|
+
// logger.info/warn/error calls still fire. Dev keeps it for debugging.
|
|
3155
|
+
disableRequestLogging: isProd,
|
|
3156
|
+
logger: {
|
|
3157
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
3158
|
+
transport: isProd ? void 0 : { target: "pino/file", options: { destination: 2 } }
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
const manager = new SessionManager(logger);
|
|
3162
|
+
const startedAt = Date.now();
|
|
3163
|
+
await app.register(cors, { origin: true });
|
|
3164
|
+
await app.register(websocket, {
|
|
3165
|
+
options: { maxPayload: 16 * 1024 * 1024 }
|
|
3166
|
+
});
|
|
3167
|
+
await app.register(multipart, {
|
|
3168
|
+
limits: { fileSize: config.maxUploadBytes, files: 20 }
|
|
3169
|
+
});
|
|
3170
|
+
await registerAuth(app);
|
|
3171
|
+
registerHealthRoute(app, manager, startedAt);
|
|
3172
|
+
registerModelRoutes(app);
|
|
3173
|
+
registerAgentRoutes(app);
|
|
3174
|
+
registerFsRoutes(app);
|
|
3175
|
+
registerSessionRoutes(app, manager);
|
|
3176
|
+
registerWorkspaceRoutes(app, manager);
|
|
3177
|
+
registerUploadRoutes(app, manager);
|
|
3178
|
+
registerWsGateway(app, manager);
|
|
3179
|
+
if (fs12.existsSync(config.webDist)) {
|
|
3180
|
+
await app.register(fastifyStatic, {
|
|
3181
|
+
root: config.webDist
|
|
3182
|
+
});
|
|
3183
|
+
app.setNotFoundHandler((req, reply) => {
|
|
3184
|
+
const path16 = req.url.split("?")[0] ?? "";
|
|
3185
|
+
const looksLikeAsset = path16.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path16);
|
|
3186
|
+
if (path16.startsWith("/api/") || path16.startsWith("/ws") || looksLikeAsset) {
|
|
3187
|
+
reply.code(404).send({ error: "Not found" });
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
reply.sendFile("index.html");
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
return { app, manager };
|
|
3194
|
+
}
|
|
3195
|
+
var init_app = __esm({
|
|
3196
|
+
"server/src/app.ts"() {
|
|
3197
|
+
"use strict";
|
|
3198
|
+
init_config();
|
|
3199
|
+
init_logger();
|
|
3200
|
+
init_SessionManager();
|
|
3201
|
+
init_auth();
|
|
3202
|
+
init_models2();
|
|
3203
|
+
init_agents2();
|
|
3204
|
+
init_fs();
|
|
3205
|
+
init_sessions();
|
|
3206
|
+
init_workspace();
|
|
3207
|
+
init_uploads();
|
|
3208
|
+
init_health();
|
|
3209
|
+
init_gateway();
|
|
3210
|
+
}
|
|
3211
|
+
});
|
|
3212
|
+
|
|
3213
|
+
// server/src/server.ts
|
|
3214
|
+
var server_exports = {};
|
|
3215
|
+
__export(server_exports, {
|
|
3216
|
+
serve: () => serve
|
|
3217
|
+
});
|
|
3218
|
+
async function serve() {
|
|
3219
|
+
if (!isWithinRoot(config.fileRoot, config.defaultCwd)) {
|
|
3220
|
+
logger.warn(
|
|
3221
|
+
{ fileRoot: config.fileRoot, defaultCwd: config.defaultCwd },
|
|
3222
|
+
"DEFAULT_CWD is outside CASPER_FILE_ROOT; new sessions and the directory picker will be rejected. Set CASPER_FILE_ROOT to an ancestor of DEFAULT_CWD."
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
const { app, manager } = await buildApp();
|
|
3226
|
+
await app.listen({ host: config.host, port: config.port });
|
|
3227
|
+
logger.info(
|
|
3228
|
+
{ host: config.host, port: config.port, auth: config.token ? "on" : "OFF" },
|
|
3229
|
+
"Casper server listening"
|
|
3230
|
+
);
|
|
3231
|
+
if (!config.token) {
|
|
3232
|
+
logger.warn("CASPER_TOKEN is empty - auth is DISABLED. Set it before exposing on a network.");
|
|
3233
|
+
}
|
|
3234
|
+
const shutdown = async (signal) => {
|
|
3235
|
+
logger.info({ signal }, "shutting down; draining live sessions");
|
|
3236
|
+
manager.disposeAll();
|
|
3237
|
+
await app.close();
|
|
3238
|
+
process.exit(0);
|
|
3239
|
+
};
|
|
3240
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
3241
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
3242
|
+
}
|
|
3243
|
+
var init_server = __esm({
|
|
3244
|
+
"server/src/server.ts"() {
|
|
3245
|
+
"use strict";
|
|
3246
|
+
init_app();
|
|
3247
|
+
init_config();
|
|
3248
|
+
init_logger();
|
|
3249
|
+
init_paths2();
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
|
|
3253
|
+
// server/src/cli/doctor.ts
|
|
3254
|
+
var doctor_exports = {};
|
|
3255
|
+
__export(doctor_exports, {
|
|
3256
|
+
doctor: () => doctor
|
|
3257
|
+
});
|
|
3258
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3259
|
+
import fs13 from "node:fs";
|
|
3260
|
+
import path14 from "node:path";
|
|
3261
|
+
function which(bin) {
|
|
3262
|
+
if (bin.includes("/")) return fs13.existsSync(bin) ? bin : void 0;
|
|
3263
|
+
for (const dir of (process.env.PATH ?? "").split(path14.delimiter)) {
|
|
3264
|
+
if (dir === "") continue;
|
|
3265
|
+
const candidate = path14.join(dir, bin);
|
|
3266
|
+
try {
|
|
3267
|
+
fs13.accessSync(candidate, fs13.constants.X_OK);
|
|
3268
|
+
return candidate;
|
|
3269
|
+
} catch {
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
return void 0;
|
|
3273
|
+
}
|
|
3274
|
+
function checks() {
|
|
3275
|
+
const out = [];
|
|
3276
|
+
out.push({
|
|
3277
|
+
ok: true,
|
|
3278
|
+
name: "node",
|
|
3279
|
+
detail: `${process.version} at ${process.execPath}`
|
|
3280
|
+
});
|
|
3281
|
+
const kiro = which(config.kiroBin);
|
|
3282
|
+
if (kiro) {
|
|
3283
|
+
let version = "";
|
|
3284
|
+
try {
|
|
3285
|
+
version = execFileSync3(config.kiroBin, ["--version"], { encoding: "utf8" }).trim();
|
|
3286
|
+
} catch {
|
|
3287
|
+
version = "(version unavailable)";
|
|
3288
|
+
}
|
|
3289
|
+
out.push({ ok: true, name: "kiro-cli", detail: `${version} at ${kiro}` });
|
|
3290
|
+
} else {
|
|
3291
|
+
out.push({
|
|
3292
|
+
ok: false,
|
|
3293
|
+
fatal: true,
|
|
3294
|
+
name: "kiro-cli",
|
|
3295
|
+
detail: `not found on PATH (looked for "${config.kiroBin}"). Casper is a client for it, so nothing works without it.`
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
const settings = readSettings(config.configFile);
|
|
3299
|
+
const hasToken = typeof settings.token === "string" && settings.token !== "";
|
|
3300
|
+
out.push(
|
|
3301
|
+
fs13.existsSync(config.configFile) ? {
|
|
3302
|
+
ok: hasToken,
|
|
3303
|
+
name: "settings",
|
|
3304
|
+
detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
|
|
3305
|
+
} : {
|
|
3306
|
+
ok: false,
|
|
3307
|
+
name: "settings",
|
|
3308
|
+
detail: `${config.configFile} not found; a token is generated on first run`
|
|
3309
|
+
}
|
|
3310
|
+
);
|
|
3311
|
+
if (fs13.existsSync(config.configFile)) {
|
|
3312
|
+
const mode = fs13.statSync(config.configFile).mode & 511;
|
|
3313
|
+
out.push({
|
|
3314
|
+
ok: mode === 384,
|
|
3315
|
+
name: "settings mode",
|
|
3316
|
+
detail: mode === 384 ? "0600" : `${mode.toString(8).padStart(3, "0")} - it holds the token; chmod 600 it`
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
try {
|
|
3320
|
+
fs13.mkdirSync(config.casperDataDir, { recursive: true });
|
|
3321
|
+
fs13.accessSync(config.casperDataDir, fs13.constants.W_OK);
|
|
3322
|
+
const db2 = path14.join(config.casperDataDir, "casper.db");
|
|
3323
|
+
const size = fs13.existsSync(db2) ? `, casper.db ${(fs13.statSync(db2).size / 1024).toFixed(0)} KB` : "";
|
|
3324
|
+
out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
|
|
3325
|
+
} catch (err) {
|
|
3326
|
+
out.push({
|
|
3327
|
+
ok: false,
|
|
3328
|
+
fatal: true,
|
|
3329
|
+
name: "data dir",
|
|
3330
|
+
detail: `${config.casperDataDir} is not writable: ${err.message}`
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
out.push(
|
|
3334
|
+
fs13.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
|
|
3335
|
+
ok: false,
|
|
3336
|
+
fatal: true,
|
|
3337
|
+
name: "web app",
|
|
3338
|
+
detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
|
|
3339
|
+
}
|
|
3340
|
+
);
|
|
3341
|
+
const agent = path14.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
|
|
3342
|
+
out.push({
|
|
3343
|
+
ok: fs13.existsSync(agent),
|
|
3344
|
+
name: "casper agent",
|
|
3345
|
+
detail: fs13.existsSync(agent) ? agent : `${agent} missing - it is installed on first run`
|
|
3346
|
+
});
|
|
3347
|
+
return out;
|
|
3348
|
+
}
|
|
3349
|
+
function doctor() {
|
|
3350
|
+
const results = checks();
|
|
3351
|
+
for (const c of results) {
|
|
3352
|
+
const mark = c.ok ? "\x1B[32m\u2713\x1B[0m" : c.fatal ? "\x1B[31m\u2717\x1B[0m" : "\x1B[33m!\x1B[0m";
|
|
3353
|
+
process.stdout.write(`${mark} ${c.name.padEnd(14)} ${c.detail}
|
|
3354
|
+
`);
|
|
3355
|
+
}
|
|
3356
|
+
const fatal = results.filter((c) => !c.ok && c.fatal);
|
|
3357
|
+
if (fatal.length > 0) {
|
|
3358
|
+
process.stdout.write(`
|
|
3359
|
+
${fatal.length} problem(s) will stop Casper from working.
|
|
3360
|
+
`);
|
|
3361
|
+
return 1;
|
|
3362
|
+
}
|
|
3363
|
+
return 0;
|
|
3364
|
+
}
|
|
3365
|
+
var init_doctor = __esm({
|
|
3366
|
+
"server/src/cli/doctor.ts"() {
|
|
3367
|
+
"use strict";
|
|
3368
|
+
init_config();
|
|
3369
|
+
init_settings();
|
|
3370
|
+
}
|
|
3371
|
+
});
|
|
3372
|
+
|
|
3373
|
+
// server/src/cli/index.ts
|
|
3374
|
+
init_paths();
|
|
3375
|
+
init_settings();
|
|
3376
|
+
import fs14 from "node:fs";
|
|
3377
|
+
import os4 from "node:os";
|
|
3378
|
+
import path15 from "node:path";
|
|
3379
|
+
var VERSION = true ? "0.1.0" : "dev";
|
|
3380
|
+
var USAGE = `casper - web client for kiro-cli
|
|
3381
|
+
|
|
3382
|
+
Usage:
|
|
3383
|
+
casper run the server in the foreground
|
|
3384
|
+
casper token print the access token
|
|
3385
|
+
casper reset-token [value] set a new token and sign every device out
|
|
3386
|
+
casper doctor check kiro-cli, settings, data dir and web app
|
|
3387
|
+
casper service install run Casper as a systemd user service
|
|
3388
|
+
casper service uninstall remove the service
|
|
3389
|
+
casper service status show the service status
|
|
3390
|
+
|
|
3391
|
+
Settings live in ${configFilePath()}.
|
|
3392
|
+
Update with: npm install -g @joeyshi12/casper
|
|
3393
|
+
`;
|
|
3394
|
+
async function bootstrap() {
|
|
3395
|
+
const file = configFilePath();
|
|
3396
|
+
const settings = readSettings(file);
|
|
3397
|
+
if (typeof settings.token !== "string" || settings.token === "") {
|
|
3398
|
+
const token = generateToken();
|
|
3399
|
+
updateSettings(file, { token });
|
|
3400
|
+
printTokenBlock(token, "Your Casper access token (paste it at the login screen):");
|
|
3401
|
+
process.stdout.write(`Saved to ${file}. Print it again with: casper token
|
|
3402
|
+
|
|
3403
|
+
`);
|
|
3404
|
+
}
|
|
3405
|
+
const { installAgentFile: installAgentFile2 } = await Promise.resolve().then(() => (init_agentFile(), agentFile_exports));
|
|
3406
|
+
const agent = installAgentFile2(os4.homedir(), dataDirPath());
|
|
3407
|
+
if (agent.action === "installed" || agent.action === "updated") {
|
|
3408
|
+
process.stdout.write(`casper agent ${agent.action}: ${agent.target}
|
|
3409
|
+
`);
|
|
3410
|
+
} else if (agent.action === "kept-yours") {
|
|
3411
|
+
process.stdout.write(`keeping your edited agent file at ${agent.target}
|
|
3412
|
+
`);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
function printToken() {
|
|
3416
|
+
const file = configFilePath();
|
|
3417
|
+
const token = readSettings(file).token;
|
|
3418
|
+
if (typeof token !== "string" || token === "") {
|
|
3419
|
+
process.stderr.write(
|
|
3420
|
+
`casper: no token in ${file} - authentication is disabled.
|
|
3421
|
+
One is generated the first time you run \`casper\`.
|
|
3422
|
+
`
|
|
3423
|
+
);
|
|
3424
|
+
return 1;
|
|
3425
|
+
}
|
|
3426
|
+
process.stdout.write(`${token}
|
|
3427
|
+
`);
|
|
3428
|
+
return 0;
|
|
3429
|
+
}
|
|
3430
|
+
async function resetToken(value) {
|
|
3431
|
+
const token = value && value !== "" ? value : generateToken();
|
|
3432
|
+
const file = configFilePath();
|
|
3433
|
+
updateSettings(file, { token });
|
|
3434
|
+
process.stdout.write(`wrote a new token to ${file}
|
|
3435
|
+
`);
|
|
3436
|
+
const dbFile = path15.join(dataDirPath(), "casper.db");
|
|
3437
|
+
if (fs14.existsSync(dbFile)) {
|
|
3438
|
+
const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
|
|
3439
|
+
new LoginStore2().revokeAll();
|
|
3440
|
+
process.stdout.write("revoked all device sessions\n");
|
|
3441
|
+
}
|
|
3442
|
+
if ((await Promise.resolve().then(() => (init_service(), service_exports))).serviceActive()) {
|
|
3443
|
+
const { spawnSync: spawnSync2 } = await import("node:child_process");
|
|
3444
|
+
spawnSync2("systemctl", ["--user", "restart", "casper.service"], { stdio: "ignore" });
|
|
3445
|
+
process.stdout.write("restarted the service\n");
|
|
3446
|
+
} else {
|
|
3447
|
+
process.stdout.write("restart Casper for the new token to take effect\n");
|
|
3448
|
+
}
|
|
3449
|
+
printTokenBlock(token, "New access token:");
|
|
3450
|
+
return 0;
|
|
3451
|
+
}
|
|
3452
|
+
async function main(argv) {
|
|
3453
|
+
const [cmd, ...rest] = argv;
|
|
3454
|
+
switch (cmd) {
|
|
3455
|
+
case void 0:
|
|
3456
|
+
case "start": {
|
|
3457
|
+
await bootstrap();
|
|
3458
|
+
const { serve: serve2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
3459
|
+
await serve2();
|
|
3460
|
+
return 0;
|
|
3461
|
+
}
|
|
3462
|
+
case "token":
|
|
3463
|
+
return printToken();
|
|
3464
|
+
case "reset-token":
|
|
3465
|
+
return resetToken(rest[0]);
|
|
3466
|
+
case "doctor":
|
|
3467
|
+
return (await Promise.resolve().then(() => (init_doctor(), doctor_exports))).doctor();
|
|
3468
|
+
case "service": {
|
|
3469
|
+
const svc = await Promise.resolve().then(() => (init_service(), service_exports));
|
|
3470
|
+
switch (rest[0]) {
|
|
3471
|
+
case "install":
|
|
3472
|
+
return svc.serviceInstall();
|
|
3473
|
+
case "uninstall":
|
|
3474
|
+
return svc.serviceUninstall();
|
|
3475
|
+
case "status":
|
|
3476
|
+
case void 0:
|
|
3477
|
+
return svc.serviceStatus();
|
|
3478
|
+
default:
|
|
3479
|
+
process.stderr.write(`casper: unknown service command "${rest[0]}"
|
|
3480
|
+
`);
|
|
3481
|
+
return 2;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
case "help":
|
|
3485
|
+
case "--help":
|
|
3486
|
+
case "-h":
|
|
3487
|
+
process.stdout.write(USAGE);
|
|
3488
|
+
return 0;
|
|
3489
|
+
case "version":
|
|
3490
|
+
case "--version":
|
|
3491
|
+
case "-v":
|
|
3492
|
+
process.stdout.write(`${VERSION}
|
|
3493
|
+
`);
|
|
3494
|
+
return 0;
|
|
3495
|
+
default:
|
|
3496
|
+
process.stderr.write(`casper: unknown command "${cmd}"
|
|
3497
|
+
|
|
3498
|
+
${USAGE}`);
|
|
3499
|
+
return 2;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
var code = await main(process.argv.slice(2));
|
|
3503
|
+
if (code !== 0) process.exit(code);
|
|
3504
|
+
export {
|
|
3505
|
+
main
|
|
3506
|
+
};
|