@thaut/claude-ide 0.0.0-stage → 1.0.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/bin/claude-ide.js +23 -0
- package/package.json +75 -4
- package/server/add-dirs.ts +126 -0
- package/server/auth.test.ts +148 -0
- package/server/auth.ts +235 -0
- package/server/bang.ts +54 -0
- package/server/btw-instructions.ts +19 -0
- package/server/btw-policy.ts +17 -0
- package/server/btw.ts +143 -0
- package/server/child-env.ts +29 -0
- package/server/cli-control.ts +255 -0
- package/server/cli-files.ts +55 -0
- package/server/cli-info.ts +73 -0
- package/server/config/account.ts +65 -0
- package/server/config/agents.ts +186 -0
- package/server/config/mcp.ts +86 -0
- package/server/config/memory.ts +193 -0
- package/server/config/paths.ts +156 -0
- package/server/config/plugins.ts +204 -0
- package/server/config/routes.ts +95 -0
- package/server/config/settings.ts +246 -0
- package/server/config.ts +46 -0
- package/server/db.ts +121 -0
- package/server/directories.test.ts +180 -0
- package/server/directories.ts +87 -0
- package/server/dist-version.ts +69 -0
- package/server/durable-tasks.ts +61 -0
- package/server/file-transfers.ts +79 -0
- package/server/files.test.ts +127 -0
- package/server/files.ts +386 -0
- package/server/fs-queue.ts +14 -0
- package/server/fsutil.ts +144 -0
- package/server/git.ts +86 -0
- package/server/goal-state.test.ts +64 -0
- package/server/goal-state.ts +78 -0
- package/server/history-pages.test.ts +619 -0
- package/server/history-pages.ts +513 -0
- package/server/http-channel-manager.ts +468 -0
- package/server/http-channel-pull.test.ts +314 -0
- package/server/http-channel-push.test.ts +364 -0
- package/server/http-channel-registration.test.ts +291 -0
- package/server/http-channel-routes.ts +73 -0
- package/server/http-channel-store.ts +142 -0
- package/server/http-channel-test-fixture.ts +103 -0
- package/server/ide-messages-mcp.test.ts +82 -0
- package/server/ide-messages-mcp.ts +47 -0
- package/server/idecron-mcp.ts +65 -0
- package/server/idecron.test.ts +79 -0
- package/server/idecron.ts +271 -0
- package/server/index.ts +336 -0
- package/server/live-session.test.ts +1126 -0
- package/server/live-session.ts +1165 -0
- package/server/project-routes.ts +46 -0
- package/server/projects.test.ts +28 -0
- package/server/push.ts +315 -0
- package/server/report-cache.ts +28 -0
- package/server/session-aliases.test.ts +57 -0
- package/server/session-aliases.ts +50 -0
- package/server/session-branch.ts +71 -0
- package/server/session-dirs.ts +189 -0
- package/server/session-file.test.ts +134 -0
- package/server/session-file.ts +113 -0
- package/server/session-messaging.test.ts +297 -0
- package/server/session-messaging.ts +165 -0
- package/server/sessions.ts +638 -0
- package/server/subagents.test.ts +43 -0
- package/server/subagents.ts +58 -0
- package/server/tool-images.test.ts +76 -0
- package/server/tool-images.ts +45 -0
- package/server/turn-changes.ts +308 -0
- package/server/worktree-git.ts +87 -0
- package/server/worktrees.ts +138 -0
- package/server/ws.test.ts +195 -0
- package/server/ws.ts +197 -0
- package/shared/agent-model.ts +15 -0
- package/shared/agent-tasks.ts +12 -0
- package/shared/config.ts +175 -0
- package/shared/directories.ts +12 -0
- package/shared/files.ts +51 -0
- package/shared/http-channels.ts +63 -0
- package/shared/message-window.ts +189 -0
- package/shared/project.ts +77 -0
- package/shared/protocol.ts +203 -0
- package/shared/task-types.ts +4 -0
- package/shared/todos.ts +40 -0
- package/shared/write-policy.ts +9 -0
- package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
- package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
- package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
- package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
- package/web/dist/assets/Form-BKTwiYyw.js +1 -0
- package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
- package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
- package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
- package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
- package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
- package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
- package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
- package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
- package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
- package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
- package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-xBchdRhy.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-1q6DGuN3.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
- package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
- package/web/dist/assets/c-D9gIxSfx.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DRAZz75c.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-CuseGurG.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/core-BxI4V4-T.js +12 -0
- package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
- package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-DpFbYmRh.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-DXQWmXTB.js +1 -0
- package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
- package/web/dist/assets/elm-BslFEg8K.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
- package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
- package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/format-DrBC7w0A.js +1 -0
- package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
- package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
- package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
- package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
- package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-CV_puxbB.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-C5GPjiLV.js +1 -0
- package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
- package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CmHiEKV9.js +1 -0
- package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
- package/web/dist/assets/http-DPjjqtcL.js +1 -0
- package/web/dist/assets/hurl-AMBpSloO.js +1 -0
- package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DNPnl4sh.css +2 -0
- package/web/dist/assets/index-DQh72lEx.js +14 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-CEM7Tdw_.js +1 -0
- package/web/dist/assets/javascript-CU3PLowd.js +1 -0
- package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
- package/web/dist/assets/jison-BoKsxs5M.js +1 -0
- package/web/dist/assets/json-BM2FWsRB.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-B5fTecQP.js +1 -0
- package/web/dist/assets/julia-DGyDG39_.js +1 -0
- package/web/dist/assets/just-B-RyLcSR.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/langs-CciPKqaF.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-CV1pQelm.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/lib-DlcVMUqT.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-CIPVh0S-.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-h2h6YUMS.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-jWINucNT.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
- package/web/dist/assets/nix-Cl_7024t.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-ClD5huOa.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-BFegxyeJ.js +1 -0
- package/web/dist/assets/php-B-qOA3Y4.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-CYUsa87k.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-4xJhgbOf.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-BlHaOHx8.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/react-B4UGpcWC.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-zjfguEh7.js +1 -0
- package/web/dist/assets/ruby-4oR9waBK.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-Do4V_2i9.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CW6JPtag.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
- package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
- package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-DVtw20Yn.js +1 -0
- package/web/dist/assets/sparql-C0ia5K60.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-DGCM-jag.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-DfuMOXna.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
- package/web/dist/assets/svelte-g_uCUem5.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-mx6xDwuK.js +1 -0
- package/web/dist/assets/themes-CNElGaXw.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-z04y7qR9.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-Bz57G_nl.js +1 -0
- package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CQgvQytb.js +1 -0
- package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
- package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-B49klYQ3.js +1 -0
- package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
- package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/icons/apple-touch-icon.png +0 -0
- package/web/dist/icons/badge-96.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon.svg +4 -0
- package/web/dist/icons/maskable-192.png +0 -0
- package/web/dist/icons/maskable-512.png +0 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +20 -0
- package/web/dist/sw.js +176 -0
- package/web/dist/version.json +1 -0
- package/web/src/App.tsx +97 -0
- package/web/src/agent-history-flow.test.ts +191 -0
- package/web/src/agent-history-flow.ts +58 -0
- package/web/src/agent-owners.ts +30 -0
- package/web/src/annotation-selection.test.ts +36 -0
- package/web/src/annotation-selection.ts +123 -0
- package/web/src/annotations.test.ts +279 -0
- package/web/src/annotations.ts +191 -0
- package/web/src/api.ts +27 -0
- package/web/src/auth-events.ts +11 -0
- package/web/src/auth-lifecycle.test.ts +105 -0
- package/web/src/clipboard.ts +32 -0
- package/web/src/components/ActivityNotifier.tsx +22 -0
- package/web/src/components/AnnotationEditor.tsx +135 -0
- package/web/src/components/AnnotationSelection.tsx +122 -0
- package/web/src/components/Automation.tsx +390 -0
- package/web/src/components/CodeView.tsx +121 -0
- package/web/src/components/Composer.tsx +552 -0
- package/web/src/components/ComposerAnnotations.tsx +45 -0
- package/web/src/components/Confirmation.tsx +91 -0
- package/web/src/components/CopyButton.tsx +23 -0
- package/web/src/components/DiffView.tsx +94 -0
- package/web/src/components/DirectoryPicker.tsx +230 -0
- package/web/src/components/FileCopyMenu.tsx +58 -0
- package/web/src/components/FileEntryActions.tsx +95 -0
- package/web/src/components/ImagePreview.tsx +68 -0
- package/web/src/components/Items.tsx +608 -0
- package/web/src/components/Markdown.tsx +73 -0
- package/web/src/components/MessageList.tsx +458 -0
- package/web/src/components/Modal.tsx +72 -0
- package/web/src/components/NotificationEntry.tsx +31 -0
- package/web/src/components/NotificationSettings.tsx +226 -0
- package/web/src/components/NotificationToasts.tsx +59 -0
- package/web/src/components/OpenTabs.tsx +112 -0
- package/web/src/components/Pending.tsx +175 -0
- package/web/src/components/ProjectExtras.tsx +299 -0
- package/web/src/components/ProjectNav.tsx +47 -0
- package/web/src/components/SessionActions.tsx +271 -0
- package/web/src/components/SessionChannels.tsx +288 -0
- package/web/src/components/SessionExtras.tsx +263 -0
- package/web/src/components/SettingsLink.tsx +21 -0
- package/web/src/components/Shell.tsx +320 -0
- package/web/src/components/SvgPreview.tsx +18 -0
- package/web/src/components/TodoPanel.tsx +37 -0
- package/web/src/components/VersionNotice.tsx +40 -0
- package/web/src/components/icons.tsx +43 -0
- package/web/src/diff.ts +62 -0
- package/web/src/drafts.test.ts +80 -0
- package/web/src/drafts.ts +64 -0
- package/web/src/format.ts +5 -0
- package/web/src/highlight.worker.ts +49 -0
- package/web/src/hooks.ts +59 -0
- package/web/src/item-view-state.test.ts +51 -0
- package/web/src/item-view-state.ts +43 -0
- package/web/src/live-history-flow.test.ts +77 -0
- package/web/src/live-merge.test.ts +64 -0
- package/web/src/live-merge.ts +50 -0
- package/web/src/live-message-hydration.test.ts +253 -0
- package/web/src/live.ts +404 -0
- package/web/src/main.tsx +27 -0
- package/web/src/markdown-assets.test.ts +70 -0
- package/web/src/markdown-assets.ts +78 -0
- package/web/src/markdown-html.test.ts +84 -0
- package/web/src/markdown-html.ts +60 -0
- package/web/src/markdown-svg.test.ts +37 -0
- package/web/src/markdown-svg.ts +42 -0
- package/web/src/markdown-tables.test.ts +34 -0
- package/web/src/markdown-tables.ts +17 -0
- package/web/src/message-bodies.ts +117 -0
- package/web/src/message-cache.test.ts +208 -0
- package/web/src/message-cache.ts +158 -0
- package/web/src/message-font-size.test.ts +41 -0
- package/web/src/message-font-size.ts +57 -0
- package/web/src/message-time.test.ts +32 -0
- package/web/src/message-time.ts +26 -0
- package/web/src/notify.ts +132 -0
- package/web/src/open-tabs.test.ts +69 -0
- package/web/src/open-tabs.ts +110 -0
- package/web/src/pages/AgentView.tsx +138 -0
- package/web/src/pages/ChangesView.tsx +193 -0
- package/web/src/pages/FilesView.tsx +406 -0
- package/web/src/pages/HomePage.tsx +96 -0
- package/web/src/pages/LoginPage.tsx +56 -0
- package/web/src/pages/OverviewPage.tsx +124 -0
- package/web/src/pages/ProjectHome.tsx +136 -0
- package/web/src/pages/SessionView.tsx +238 -0
- package/web/src/pages/WorktreesView.tsx +197 -0
- package/web/src/pages/settings/AccountPage.tsx +149 -0
- package/web/src/pages/settings/AgentsPage.tsx +182 -0
- package/web/src/pages/settings/ConfigPage.tsx +181 -0
- package/web/src/pages/settings/GeneralPage.tsx +27 -0
- package/web/src/pages/settings/HooksPage.tsx +244 -0
- package/web/src/pages/settings/McpPage.tsx +310 -0
- package/web/src/pages/settings/MemoryPage.tsx +309 -0
- package/web/src/pages/settings/PermissionsPage.tsx +160 -0
- package/web/src/pages/settings/PluginsPage.tsx +251 -0
- package/web/src/pages/settings/SettingsPage.tsx +141 -0
- package/web/src/pages/settings/SkillsPage.tsx +142 -0
- package/web/src/pages/settings/ui.tsx +165 -0
- package/web/src/preference-document.ts +133 -0
- package/web/src/project-actions.ts +18 -0
- package/web/src/project-files-state.test.ts +49 -0
- package/web/src/project-files-state.ts +64 -0
- package/web/src/project-images.ts +13 -0
- package/web/src/pwa.ts +33 -0
- package/web/src/queryClient.ts +5 -0
- package/web/src/reasoning-preference.test.ts +39 -0
- package/web/src/reasoning-preference.ts +42 -0
- package/web/src/session-confirmations.ts +36 -0
- package/web/src/stream-replay.test.ts +163 -0
- package/web/src/styles.css +231 -0
- package/web/src/tool-result-body.test.ts +115 -0
- package/web/src/transcript.test.ts +186 -0
- package/web/src/transcript.ts +583 -0
- package/web/src/turns.test.ts +338 -0
- package/web/src/turns.ts +178 -0
- package/web/src/ui/Button.tsx +60 -0
- package/web/src/ui/Controls.tsx +227 -0
- package/web/src/ui/Form.tsx +203 -0
- package/web/src/ui/MenuPopover.tsx +144 -0
- package/web/src/ui/Popover.tsx +147 -0
- package/web/src/ui/ResizablePane.tsx +110 -0
- package/web/src/ui/Select.tsx +150 -0
- package/web/src/ui/icons.tsx +270 -0
- package/web/src/unsaved.tsx +99 -0
- package/web/src/user-text.test.ts +60 -0
- package/web/src/user-text.ts +35 -0
- package/web/src/uuid.ts +12 -0
- package/README.md +0 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const entry = path.join(root, "server", "index.ts");
|
|
9
|
+
|
|
10
|
+
// Resolve tsx from this package's own dependencies, not from the user's cwd.
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const tsxLoader = pathToFileURL(require.resolve("tsx")).href;
|
|
13
|
+
|
|
14
|
+
const child = spawn(process.execPath, ["--import", tsxLoader, entry, ...process.argv.slice(2)], {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "production" },
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
21
|
+
process.on(sig, () => child.kill(sig));
|
|
22
|
+
}
|
|
23
|
+
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thaut/claude-ide",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude IDE: web UI for Claude Code sessions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"dev": "concurrently -k -n server,web \"npm:dev:server\" \"vite\"",
|
|
9
|
+
"build": "tsc --noEmit -p . && vite build",
|
|
10
|
+
"start": "vite build && NODE_ENV=production tsx watch server/index.ts",
|
|
11
|
+
"typecheck": "tsc --noEmit -p .",
|
|
12
|
+
"test:e2e": "npm run test:unit && npm run build && (E2E_GROUP=parallel playwright test; a=$?; E2E_GROUP=serial playwright test; b=$?; exit $(( a != 0 || b != 0 )))",
|
|
13
|
+
"dev:server": "bash -c 'while true; do tsx watch server/index.ts; echo \"[dev] server exited, restarting in 2s\"; sleep 2; done'",
|
|
14
|
+
"test:e2e:live": "npm run build && E2E_MODE=live playwright test",
|
|
15
|
+
"e2e:fixtures": "tsx e2e/replay/build-fixtures.ts",
|
|
16
|
+
"test:e2e:record": "npm run build && E2E_MODE=record playwright test",
|
|
17
|
+
"test:unit": "node --import tsx --test 'server/**/*.test.ts' 'web/src/**/*.test.ts'",
|
|
18
|
+
"test:http-channels": "node --import tsx --test --test-concurrency=3 server/http-channel-registration.test.ts server/http-channel-pull.test.ts server/http-channel-push.test.ts",
|
|
19
|
+
"prepack": "vite build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.296",
|
|
27
|
+
"@hono/node-server": "^1.19.17",
|
|
28
|
+
"@hono/node-ws": "^1.3.1",
|
|
29
|
+
"@tanstack/react-query": "^5.104.2",
|
|
30
|
+
"@tanstack/react-virtual": "^3.14.19",
|
|
31
|
+
"cron-parser": "^5.10.2",
|
|
32
|
+
"hast-util-to-html": "^9.0.6",
|
|
33
|
+
"hono": "^4.13.13",
|
|
34
|
+
"mdast-util-gfm": "^3.1.0",
|
|
35
|
+
"mdast-util-to-markdown": "^2.2.0",
|
|
36
|
+
"react": "^19.3.0",
|
|
37
|
+
"react-dom": "^19.3.0",
|
|
38
|
+
"react-markdown": "^10.1.0",
|
|
39
|
+
"react-router": "^8.4.0",
|
|
40
|
+
"rehype-raw": "^7.0.0",
|
|
41
|
+
"rehype-sanitize": "^6.0.0",
|
|
42
|
+
"remark-gfm": "^4.0.1",
|
|
43
|
+
"shiki": "^4.5.0",
|
|
44
|
+
"web-push": "^3.6.7",
|
|
45
|
+
"zod": "^4.6.7",
|
|
46
|
+
"zustand": "^5.0.15",
|
|
47
|
+
"tsx": "^4.23.15"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@playwright/test": "^1.64.0",
|
|
51
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
52
|
+
"@types/mdast": "^4.0.4",
|
|
53
|
+
"@types/node": "^26.6.5",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
55
|
+
"@types/react-dom": "^19.3.0",
|
|
56
|
+
"@types/web-push": "^3.6.4",
|
|
57
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
58
|
+
"concurrently": "^10.0.6",
|
|
59
|
+
"remark-parse": "^11.0.0",
|
|
60
|
+
"tailwindcss": "^4.3.3",
|
|
61
|
+
"typescript": "^7.0.2",
|
|
62
|
+
"unified": "^11.0.5",
|
|
63
|
+
"vite": "^8.3.4"
|
|
64
|
+
},
|
|
65
|
+
"bin": {
|
|
66
|
+
"claude-ide": "bin/claude-ide.js"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"bin/",
|
|
70
|
+
"server/",
|
|
71
|
+
"shared/",
|
|
72
|
+
"web/src/",
|
|
73
|
+
"web/dist/",
|
|
74
|
+
"README.md",
|
|
75
|
+
"package.json"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { ExtraDir, ExtraDirSource, ExtraDirsInfo } from "../shared/project.ts";
|
|
5
|
+
import { claudeDir } from "./cli-files.ts";
|
|
6
|
+
import { deleteSessionDir, insertSessionDir, sessionCwd, sessionDirs } from "./session-dirs.ts";
|
|
7
|
+
import { HttpError, sessions } from "./sessions.ts";
|
|
8
|
+
import { withFileLock } from "./fs-queue.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* /add-dir (see NOTES.md "CLI 配置文件"): directories outside the project a session may use.
|
|
12
|
+
* - Session-only dirs are kept in sqlite and handed to the CLI through the flag-settings layer
|
|
13
|
+
* (`applyFlagSettings({permissions: {additionalDirectories}})`) at start and on every change,
|
|
14
|
+
* which also works while the CLI process runs, removal included [实测 m4_add_dir.mjs].
|
|
15
|
+
* - "Remember" writes `permissions.additionalDirectories` of the session cwd's
|
|
16
|
+
* `.claude/settings.local.json`, exactly as the CLI's /add-dir does.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type SettingsFile = { permissions?: { additionalDirectories?: unknown } & Record<string, unknown> } & Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
const localSettingsFile = (cwd: string) => path.join(cwd, ".claude", "settings.local.json");
|
|
22
|
+
|
|
23
|
+
async function exists(p: string) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(p);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readJson(file: string): Promise<SettingsFile | undefined> {
|
|
33
|
+
try {
|
|
34
|
+
const v = JSON.parse(await fs.readFile(file, "utf8"));
|
|
35
|
+
return v && typeof v === "object" ? v : undefined;
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function dirsIn(file: string): Promise<string[]> {
|
|
42
|
+
const list = (await readJson(file))?.permissions?.additionalDirectories;
|
|
43
|
+
return Array.isArray(list) ? list.filter((x): x is string => typeof x === "string") : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read-modify-write of settings.local.json, keeping every other key; written atomically. */
|
|
47
|
+
function updateLocalDirs(cwd: string, fn: (dirs: string[]) => string[]) {
|
|
48
|
+
const file = localSettingsFile(cwd);
|
|
49
|
+
return withFileLock(file, async () => {
|
|
50
|
+
let data: SettingsFile = {};
|
|
51
|
+
if (await exists(file)) {
|
|
52
|
+
const parsed = await readJson(file);
|
|
53
|
+
if (!parsed) throw new HttpError(409, `${file} 不是有效的 JSON,请先修好它`);
|
|
54
|
+
data = parsed;
|
|
55
|
+
}
|
|
56
|
+
const next = fn(await dirsIn(file));
|
|
57
|
+
const permissions = { ...(data.permissions ?? {}) };
|
|
58
|
+
if (next.length) permissions.additionalDirectories = next;
|
|
59
|
+
else delete permissions.additionalDirectories;
|
|
60
|
+
data = { ...data, permissions };
|
|
61
|
+
if (Object.keys(permissions).length === 0) delete data.permissions;
|
|
62
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
63
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
64
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
65
|
+
await fs.rename(tmp, file);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const expandHome = (p: string) => (p === "~" ? os.homedir() : p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p);
|
|
70
|
+
|
|
71
|
+
/** Settings entries may be relative to the directory holding `.claude/`. */
|
|
72
|
+
const resolveEntry = (base: string, p: string) => path.resolve(base, expandHome(p));
|
|
73
|
+
|
|
74
|
+
export async function extraDirs(projectPath: string, sessionId: string): Promise<ExtraDirsInfo> {
|
|
75
|
+
const cwd = await sessionCwd(projectPath, sessionId);
|
|
76
|
+
const map = new Map<string, ExtraDirSource[]>();
|
|
77
|
+
const add = (p: string, s: ExtraDirSource) => {
|
|
78
|
+
const list = map.get(p) ?? [];
|
|
79
|
+
if (!list.includes(s)) list.push(s);
|
|
80
|
+
map.set(p, list);
|
|
81
|
+
};
|
|
82
|
+
for (const d of sessionDirs(sessionId)) add(d, "session");
|
|
83
|
+
for (const d of await dirsIn(localSettingsFile(cwd))) add(resolveEntry(cwd, d), "local");
|
|
84
|
+
for (const d of await dirsIn(path.join(cwd, ".claude", "settings.json"))) add(resolveEntry(cwd, d), "project");
|
|
85
|
+
for (const d of await dirsIn(path.join(claudeDir(), "settings.json"))) add(resolveEntry(os.homedir(), d), "user");
|
|
86
|
+
const dirs: ExtraDir[] = await Promise.all([...map].map(async ([p, sources]) => ({ path: p, sources, exists: await isDir(p) })));
|
|
87
|
+
return { cwd, dirs, localSettingsFile: localSettingsFile(cwd) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function isDir(p: string) {
|
|
91
|
+
try {
|
|
92
|
+
return (await fs.stat(p)).isDirectory();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Push the session's list to its running CLI process (no-op when it is not running). */
|
|
99
|
+
async function applyLive(sessionId: string) {
|
|
100
|
+
await sessions.get(sessionId)?.setAdditionalDirectories(sessionDirs(sessionId));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function addExtraDir(projectPath: string, sessionId: string, raw: string, remember: boolean): Promise<ExtraDirsInfo> {
|
|
104
|
+
const input = expandHome(raw.trim());
|
|
105
|
+
if (!input) throw new HttpError(400, "请填写目录路径");
|
|
106
|
+
if (!path.isAbsolute(input)) throw new HttpError(400, "请填写绝对路径");
|
|
107
|
+
if (!(await isDir(input))) throw new HttpError(400, "目录不存在");
|
|
108
|
+
const dir = await fs.realpath(input);
|
|
109
|
+
const cwd = await sessionCwd(projectPath, sessionId);
|
|
110
|
+
const rel = path.relative(await fs.realpath(cwd), dir);
|
|
111
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) throw new HttpError(400, "这个目录已经在会话的工作目录里了");
|
|
112
|
+
insertSessionDir(sessionId, dir);
|
|
113
|
+
if (remember) await updateLocalDirs(cwd, (dirs) => (dirs.some((d) => resolveEntry(cwd, d) === dir) ? dirs : [...dirs, dir]));
|
|
114
|
+
await applyLive(sessionId);
|
|
115
|
+
return extraDirs(projectPath, sessionId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function removeExtraDir(projectPath: string, sessionId: string, dir: string): Promise<ExtraDirsInfo> {
|
|
119
|
+
const cwd = await sessionCwd(projectPath, sessionId);
|
|
120
|
+
deleteSessionDir(sessionId, dir);
|
|
121
|
+
if ((await dirsIn(localSettingsFile(cwd))).some((d) => resolveEntry(cwd, d) === dir)) {
|
|
122
|
+
await updateLocalDirs(cwd, (dirs) => dirs.filter((d) => resolveEntry(cwd, d) !== dir));
|
|
123
|
+
}
|
|
124
|
+
await applyLive(sessionId);
|
|
125
|
+
return extraDirs(projectPath, sessionId);
|
|
126
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { test, after } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
|
|
10
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cide-auth-unit-"));
|
|
11
|
+
process.env.CLAUDE_IDE_DATA_DIR = dir;
|
|
12
|
+
process.env.CLAUDE_IDE_PASSWORD = "unused-environment-override";
|
|
13
|
+
const passwordFile = path.join(dir, "password");
|
|
14
|
+
await fs.writeFile(passwordFile, "synthetic-initial\n", { mode: 0o600 });
|
|
15
|
+
const { db } = await import("./db.ts");
|
|
16
|
+
const { AUTH_COOKIE, login, isAuthed, isTokenValid, validateToken, refreshAuth, onAuthRevoked, watchPassword } = await import("./auth.ts");
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.post("/login", async (c) => c.json({ ok: await login(c, await c.req.text()) }));
|
|
19
|
+
app.get("/auth", async (c) => c.json({ authed: await isAuthed(c) }));
|
|
20
|
+
async function signIn(password: string) {
|
|
21
|
+
const response = await app.request("/login", { method: "POST", body: password });
|
|
22
|
+
const body = await response.json() as { ok: boolean };
|
|
23
|
+
const cookie = response.headers.get("set-cookie")?.split(";")[0];
|
|
24
|
+
return { ok: body.ok, cookie, token: cookie?.slice(`${AUTH_COOKIE}=`.length) };
|
|
25
|
+
}
|
|
26
|
+
after(async () => { db.close(); await fs.rm(dir, { recursive: true, force: true }); });
|
|
27
|
+
|
|
28
|
+
test("first credential initialization revokes unbound legacy tokens once; file overrides env", async () => {
|
|
29
|
+
db.prepare("INSERT INTO auth_tokens VALUES (?, ?, ?)").run("synthetic-legacy", 1, 1);
|
|
30
|
+
assert.equal(await refreshAuth(), true);
|
|
31
|
+
assert.equal(isTokenValid("synthetic-legacy"), false);
|
|
32
|
+
assert.equal((await signIn("unused-environment-override")).ok, false);
|
|
33
|
+
assert.equal((await signIn("synthetic-initial")).ok, true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("same password rewrites preserve tokens; atomic replacement rotates every device", async () => {
|
|
37
|
+
const first = await signIn("synthetic-initial"), second = await signIn("synthetic-initial");
|
|
38
|
+
assert.ok(first.token); assert.ok(second.token);
|
|
39
|
+
await fs.writeFile(passwordFile, "synthetic-initial\n\n");
|
|
40
|
+
assert.equal(await validateToken(first.token), true);
|
|
41
|
+
await fs.writeFile(`${passwordFile}.new`, "synthetic-initial\n");
|
|
42
|
+
await fs.rename(`${passwordFile}.new`, passwordFile);
|
|
43
|
+
assert.equal(await validateToken(second.token), true);
|
|
44
|
+
let revoked = 0;
|
|
45
|
+
const stop = onAuthRevoked(() => revoked++);
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(`${passwordFile}.new`, "synthetic-rotated\n");
|
|
48
|
+
await fs.rename(`${passwordFile}.new`, passwordFile);
|
|
49
|
+
const [oldLogin, newLogin] = await Promise.all([signIn("synthetic-initial"), signIn("synthetic-rotated")]);
|
|
50
|
+
assert.equal(oldLogin.ok, false); assert.equal(newLogin.ok, true);
|
|
51
|
+
assert.equal(isTokenValid(first.token), false); assert.equal(isTokenValid(second.token), false);
|
|
52
|
+
assert.equal(revoked, 1);
|
|
53
|
+
assert.equal(await validateToken(newLogin.token!), true);
|
|
54
|
+
const response = await app.request("/auth", { headers: { cookie: first.cookie! } });
|
|
55
|
+
assert.deepEqual(await response.json(), { authed: false });
|
|
56
|
+
} finally { stop(); }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("confirmed missing, empty, and unreadable files revoke tokens without regenerating or reviving them", async () => {
|
|
60
|
+
const assertUnavailable = async (breakFile: () => Promise<void>, restore: () => Promise<void>) => {
|
|
61
|
+
const device = await signIn("synthetic-rotated");
|
|
62
|
+
assert.ok(device.token);
|
|
63
|
+
await breakFile();
|
|
64
|
+
assert.equal(await refreshAuth(), false);
|
|
65
|
+
assert.equal(isTokenValid(device.token), false);
|
|
66
|
+
assert.equal((await signIn("synthetic-rotated")).ok, false);
|
|
67
|
+
await restore();
|
|
68
|
+
assert.equal(await refreshAuth(), true);
|
|
69
|
+
assert.equal(isTokenValid(device.token), false, "restoring the same password cannot revive an old login");
|
|
70
|
+
};
|
|
71
|
+
await assertUnavailable(() => fs.unlink(passwordFile), async () => {
|
|
72
|
+
await assert.rejects(fs.stat(passwordFile), { code: "ENOENT" });
|
|
73
|
+
await fs.writeFile(passwordFile, "synthetic-rotated\n");
|
|
74
|
+
});
|
|
75
|
+
await assertUnavailable(() => fs.writeFile(passwordFile, " \n"), async () => {
|
|
76
|
+
assert.equal(await fs.readFile(passwordFile, "utf8"), " \n");
|
|
77
|
+
await fs.writeFile(passwordFile, "synthetic-rotated\n");
|
|
78
|
+
});
|
|
79
|
+
// EISDIR reliably exercises unreadability even when CI runs with root privileges.
|
|
80
|
+
await assertUnavailable(async () => {
|
|
81
|
+
await fs.unlink(passwordFile); await fs.mkdir(passwordFile);
|
|
82
|
+
}, async () => {
|
|
83
|
+
await fs.rmdir(passwordFile); await fs.writeFile(passwordFile, "synthetic-rotated\n");
|
|
84
|
+
});
|
|
85
|
+
if (process.getuid?.() !== 0) {
|
|
86
|
+
await assertUnavailable(() => fs.chmod(passwordFile, 0), () => fs.chmod(passwordFile, 0o600));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("brief editor rename gap recovers within retries without revocation", async () => {
|
|
91
|
+
const device = await signIn("synthetic-rotated");
|
|
92
|
+
await fs.rename(passwordFile, `${passwordFile}.saved`);
|
|
93
|
+
const timer = setTimeout(() => { void fs.rename(`${passwordFile}.saved`, passwordFile); }, 10);
|
|
94
|
+
try { assert.equal(await validateToken(device.token!), true); }
|
|
95
|
+
finally { clearTimeout(timer); }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("background polling revokes idle authenticated devices", async () => {
|
|
99
|
+
const device = await signIn("synthetic-rotated");
|
|
100
|
+
let revoke!: () => void;
|
|
101
|
+
const revoked = new Promise<void>((resolve) => { revoke = resolve; });
|
|
102
|
+
const unsubscribe = onAuthRevoked(revoke);
|
|
103
|
+
const stop = watchPassword(10);
|
|
104
|
+
try {
|
|
105
|
+
await fs.writeFile(passwordFile, "synthetic-background\n");
|
|
106
|
+
await Promise.race([revoked, new Promise<never>((_, reject) => {
|
|
107
|
+
const timer = setTimeout(() => reject(new Error("password poll did not revoke")), 2000); timer.unref();
|
|
108
|
+
})]);
|
|
109
|
+
assert.equal(isTokenValid(device.token!), false);
|
|
110
|
+
assert.equal((await signIn("synthetic-background")).ok, true);
|
|
111
|
+
} finally { stop(); unsubscribe(); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("persisted verifier keeps same-password restarts, revokes changed restarts, and never recreates missing deployed password", async () => {
|
|
115
|
+
const restartDir = await fs.mkdtemp(path.join(os.tmpdir(), "cide-auth-restart-"));
|
|
116
|
+
const run = promisify(execFile);
|
|
117
|
+
const authUrl = new URL("./auth.ts", import.meta.url).href;
|
|
118
|
+
const dbUrl = new URL("./db.ts", import.meta.url).href;
|
|
119
|
+
const configUrl = new URL("./config.ts", import.meta.url).href;
|
|
120
|
+
const script = `
|
|
121
|
+
import {refreshAuth,isTokenValid} from ${JSON.stringify(authUrl)};
|
|
122
|
+
import {db} from ${JSON.stringify(dbUrl)};
|
|
123
|
+
import {config} from ${JSON.stringify(configUrl)};
|
|
124
|
+
import fs from 'node:fs';
|
|
125
|
+
const ready = await refreshAuth();
|
|
126
|
+
const existingValid = isTokenValid('synthetic-restart-token');
|
|
127
|
+
if (process.env.AUDIT_INSERT === 'yes') db.prepare('INSERT INTO auth_tokens VALUES (?, ?, ?)').run('synthetic-restart-token',1,1);
|
|
128
|
+
console.log(JSON.stringify({ready,existingValid,fileExists:fs.existsSync(config.passwordFile)}));
|
|
129
|
+
db.close();
|
|
130
|
+
`;
|
|
131
|
+
const child = async (insert = false) => {
|
|
132
|
+
const { stdout } = await run(process.execPath, ["--import", "tsx", "--input-type=module", "-e", script], {
|
|
133
|
+
env: { ...process.env, CLAUDE_IDE_DATA_DIR: restartDir, CLAUDE_IDE_PASSWORD: "synthetic-seed", AUDIT_INSERT: insert ? "yes" : "no" },
|
|
134
|
+
});
|
|
135
|
+
return JSON.parse(stdout.trim()) as { ready: boolean; existingValid: boolean; fileExists: boolean };
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
assert.deepEqual(await child(true), { ready: true, existingValid: false, fileExists: true });
|
|
139
|
+
assert.deepEqual(await child(), { ready: true, existingValid: true, fileExists: true });
|
|
140
|
+
await fs.writeFile(path.join(restartDir, "password"), "synthetic-restart-change\n");
|
|
141
|
+
assert.deepEqual(await child(), { ready: true, existingValid: false, fileExists: true });
|
|
142
|
+
await child(true);
|
|
143
|
+
await fs.unlink(path.join(restartDir, "password"));
|
|
144
|
+
assert.deepEqual(await child(), { ready: false, existingValid: false, fileExists: false });
|
|
145
|
+
await fs.writeFile(path.join(restartDir, "password"), "synthetic-restart-change\n");
|
|
146
|
+
assert.deepEqual(await child(), { ready: true, existingValid: false, fileExists: true });
|
|
147
|
+
} finally { await fs.rm(restartDir, { recursive: true, force: true }); }
|
|
148
|
+
});
|
package/server/auth.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
5
|
+
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
6
|
+
import { config } from "./config.ts";
|
|
7
|
+
import { db } from "./db.ts";
|
|
8
|
+
|
|
9
|
+
export const AUTH_COOKIE = "cide_auth";
|
|
10
|
+
const ONE_YEAR = 365 * 24 * 3600;
|
|
11
|
+
const scrypt = promisify(crypto.scrypt);
|
|
12
|
+
const revocationListeners = new Set<() => void>();
|
|
13
|
+
let currentPassword: string | undefined;
|
|
14
|
+
let available = false;
|
|
15
|
+
let unavailableNotified = false;
|
|
16
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
17
|
+
|
|
18
|
+
// Refresh, validation, and token creation share one queue: a successful old-
|
|
19
|
+
// password check can never register a token after a newer generation is loaded.
|
|
20
|
+
function serialized<T>(operation: () => Promise<T>): Promise<T> {
|
|
21
|
+
const result = queue.then(operation);
|
|
22
|
+
queue = result.catch(() => {});
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function onAuthRevoked(listener: () => void): () => void {
|
|
27
|
+
revocationListeners.add(listener);
|
|
28
|
+
return () => { revocationListeners.delete(listener); };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function notifyRevoked() {
|
|
32
|
+
for (const listener of revocationListeners) listener();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function safeEqual(a: string, b: string): boolean {
|
|
36
|
+
const ha = crypto.createHash("sha256").update(a).digest();
|
|
37
|
+
const hb = crypto.createHash("sha256").update(b).digest();
|
|
38
|
+
return crypto.timingSafeEqual(ha, hb);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readPassword(): Promise<string> {
|
|
42
|
+
// Read by pathname every time, so both atomic replacement and in-place writes
|
|
43
|
+
// are observed. Brief editor rename/truncate windows get a bounded retry.
|
|
44
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
const password = (await fs.readFile(config.passwordFile, "utf8")).trim();
|
|
47
|
+
if (password) return password;
|
|
48
|
+
} catch {
|
|
49
|
+
// No cached-password fallback and no generation on credential read errors.
|
|
50
|
+
}
|
|
51
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
52
|
+
}
|
|
53
|
+
throw new Error("login password unavailable");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function refreshCredential(): Promise<boolean> {
|
|
57
|
+
let password: string;
|
|
58
|
+
try {
|
|
59
|
+
password = await readPassword();
|
|
60
|
+
} catch {
|
|
61
|
+
available = false;
|
|
62
|
+
// After bounded retries, a missing/empty/unreadable file revokes devices.
|
|
63
|
+
// Keep only the verifier: restoring the same file must not revive tokens.
|
|
64
|
+
if (!unavailableNotified) {
|
|
65
|
+
db.prepare("DELETE FROM auth_tokens").run();
|
|
66
|
+
unavailableNotified = true;
|
|
67
|
+
notifyRevoked();
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (currentPassword !== undefined && safeEqual(password, currentPassword)) {
|
|
72
|
+
available = true;
|
|
73
|
+
unavailableNotified = false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
const saved = db.prepare("SELECT salt, verifier FROM auth_credential WHERE id = 1").get() as { salt: string; verifier: string } | undefined;
|
|
77
|
+
const salt = saved?.salt ?? crypto.randomBytes(32).toString("hex");
|
|
78
|
+
const verifier = (await scrypt(password, salt, 32) as Buffer).toString("hex");
|
|
79
|
+
const changed = !saved || !safeEqual(verifier, saved.verifier);
|
|
80
|
+
if (changed) {
|
|
81
|
+
db.exec("BEGIN IMMEDIATE");
|
|
82
|
+
try {
|
|
83
|
+
// On first upgrade no trustworthy old password identifier exists. Revoke
|
|
84
|
+
// legacy tokens once; ordinary same-password restarts preserve tokens.
|
|
85
|
+
db.prepare("DELETE FROM auth_tokens").run();
|
|
86
|
+
db.prepare("INSERT INTO auth_credential (id, salt, verifier) VALUES (1, ?, ?) ON CONFLICT(id) DO UPDATE SET salt=excluded.salt, verifier=excluded.verifier").run(salt, verifier);
|
|
87
|
+
db.exec("COMMIT");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
db.exec("ROLLBACK");
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
currentPassword = password;
|
|
94
|
+
available = true;
|
|
95
|
+
unavailableNotified = false;
|
|
96
|
+
if (changed) notifyRevoked();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function refresh(): Promise<boolean> {
|
|
101
|
+
try {
|
|
102
|
+
return await refreshCredential();
|
|
103
|
+
} catch {
|
|
104
|
+
// Authentication errors must deny access, including verifier/database errors.
|
|
105
|
+
// Do not expose filesystem paths, credential material, or database details.
|
|
106
|
+
available = false;
|
|
107
|
+
if (!unavailableNotified) {
|
|
108
|
+
unavailableNotified = true;
|
|
109
|
+
notifyRevoked();
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function refreshAuth(): Promise<boolean> {
|
|
116
|
+
return serialized(refresh);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Polling also revokes idle sockets that have no incoming HTTP/WS traffic. */
|
|
120
|
+
export function watchPassword(intervalMs = 500): () => void {
|
|
121
|
+
let stopped = false;
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
const poll = async () => {
|
|
124
|
+
try { await refreshAuth(); }
|
|
125
|
+
catch { console.error("Authentication refresh failed"); }
|
|
126
|
+
if (!stopped) {
|
|
127
|
+
timer = setTimeout(() => { void poll(); }, intervalMs);
|
|
128
|
+
timer.unref();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
void poll();
|
|
132
|
+
return () => { stopped = true; if (timer) clearTimeout(timer); };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function issueToken(c: Context): void {
|
|
136
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
db.prepare("INSERT INTO auth_tokens (token, created_at, last_seen_at) VALUES (?, ?, ?)").run(token, now, now);
|
|
139
|
+
setCookie(c, AUTH_COOKIE, token, {
|
|
140
|
+
httpOnly: true,
|
|
141
|
+
sameSite: "Lax",
|
|
142
|
+
secure: c.req.url.startsWith("https:"),
|
|
143
|
+
path: "/",
|
|
144
|
+
maxAge: ONE_YEAR,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function login(c: Context, candidate: string): Promise<boolean> {
|
|
149
|
+
return serialized(async () => {
|
|
150
|
+
if (!await refresh() || !safeEqual(candidate, currentPassword!)) return false;
|
|
151
|
+
// No await separates password validation from token insertion.
|
|
152
|
+
issueToken(c);
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function authToken(c: Context): string | undefined {
|
|
158
|
+
return getCookie(c, AUTH_COOKIE);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function revokeToken(c: Context): string | undefined {
|
|
162
|
+
const token = getCookie(c, AUTH_COOKIE);
|
|
163
|
+
if (token) db.prepare("DELETE FROM auth_tokens WHERE token = ?").run(token);
|
|
164
|
+
deleteCookie(c, AUTH_COOKIE, { path: "/" });
|
|
165
|
+
return token;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Synchronous admission check after an async handshake; it catches revocation
|
|
169
|
+
// between Hono's upgrade callback and the socket's onOpen event.
|
|
170
|
+
export function isTokenValid(token: string): boolean {
|
|
171
|
+
return available && !!db.prepare("SELECT token FROM auth_tokens WHERE token = ?").get(token);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function validateToken(token: string): Promise<boolean> {
|
|
175
|
+
return serialized(async () => await refresh() && isTokenValid(token));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function isAuthed(c: Context): Promise<boolean> {
|
|
179
|
+
const token = getCookie(c, AUTH_COOKIE);
|
|
180
|
+
// Even unauthenticated probes observe password changes and revoke old sockets.
|
|
181
|
+
return serialized(async () => {
|
|
182
|
+
if (!await refresh() || !token || !isTokenValid(token)) return false;
|
|
183
|
+
db.prepare("UPDATE auth_tokens SET last_seen_at = ? WHERE token = ?").run(Date.now(), token);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const requireAuth: MiddlewareHandler = async (c, next) => {
|
|
189
|
+
if (!await isAuthed(c)) return c.json({ error: "unauthorized" }, 401);
|
|
190
|
+
await next();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Requests from a page of another origin are refused (cross-site WebSocket hijacking, CSRF).
|
|
195
|
+
* SameSite=Lax cookies already keep most of them out; this is the second lock. Requests
|
|
196
|
+
* without an Origin header (curl, the CLI, tests) are not browser cross-site requests.
|
|
197
|
+
*/
|
|
198
|
+
export function sameOrigin(c: Context): boolean {
|
|
199
|
+
const origin = c.req.header("origin");
|
|
200
|
+
if (!origin) return true;
|
|
201
|
+
try {
|
|
202
|
+
const host = new URL(origin).host;
|
|
203
|
+
// Behind a reverse proxy (e.g. Tailscale serve) the original host arrives as X-Forwarded-Host.
|
|
204
|
+
return host === c.req.header("host") || host === c.req.header("x-forwarded-host");
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Failed logins per client address: after MAX_FAILS within WINDOW, refuse for LOCK. */
|
|
211
|
+
const MAX_FAILS = 10;
|
|
212
|
+
const WINDOW_MS = 15 * 60_000;
|
|
213
|
+
const LOCK_MS = 15 * 60_000;
|
|
214
|
+
const fails = new Map<string, { count: number; first: number; lockedUntil: number }>();
|
|
215
|
+
|
|
216
|
+
export function clientAddress(c: Context): string {
|
|
217
|
+
const incoming = (c.env as { incoming?: { socket?: { remoteAddress?: string } } } | undefined)?.incoming;
|
|
218
|
+
return incoming?.socket?.remoteAddress ?? "unknown";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Seconds until this address may try again, or 0. */
|
|
222
|
+
export function loginLockedFor(addr: string): number {
|
|
223
|
+
const f = fails.get(addr);
|
|
224
|
+
return f && f.lockedUntil > Date.now() ? Math.ceil((f.lockedUntil - Date.now()) / 1000) : 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function recordLogin(addr: string, ok: boolean) {
|
|
228
|
+
if (ok) return void fails.delete(addr);
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const f = fails.get(addr);
|
|
231
|
+
const cur = f && now - f.first < WINDOW_MS ? f : { count: 0, first: now, lockedUntil: 0 };
|
|
232
|
+
cur.count++;
|
|
233
|
+
if (cur.count >= MAX_FAILS) cur.lockedUntil = now + LOCK_MS;
|
|
234
|
+
fails.set(addr, cur);
|
|
235
|
+
}
|
package/server/bang.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { childEnv } from "./child-env.ts";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `!command` from the composer: the server runs the command itself (like the CLI's bash mode)
|
|
6
|
+
* and the output is added to the conversation as a `shouldQuery: false` user message.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TIMEOUT_MS = 2 * 60_000;
|
|
10
|
+
const MAX_CHARS = 30_000;
|
|
11
|
+
|
|
12
|
+
export type BangResult = { stdout: string; stderr: string; code: number | null; timedOut: boolean };
|
|
13
|
+
|
|
14
|
+
export function runBang(command: string, cwd: string): Promise<BangResult> {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const child = spawn("bash", ["-lc", command], { cwd, stdio: ["ignore", "pipe", "pipe"], detached: true, env: childEnv() as NodeJS.ProcessEnv });
|
|
17
|
+
let stdout = "";
|
|
18
|
+
let stderr = "";
|
|
19
|
+
let timedOut = false;
|
|
20
|
+
const cap = (buf: string, chunk: Buffer) => (buf.length > MAX_CHARS * 2 ? buf : buf + chunk.toString("utf8"));
|
|
21
|
+
child.stdout.on("data", (c: Buffer) => (stdout = cap(stdout, c)));
|
|
22
|
+
child.stderr.on("data", (c: Buffer) => (stderr = cap(stderr, c)));
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
timedOut = true;
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-child.pid!, "SIGKILL"); // the whole process group
|
|
27
|
+
} catch {
|
|
28
|
+
child.kill("SIGKILL");
|
|
29
|
+
}
|
|
30
|
+
}, TIMEOUT_MS);
|
|
31
|
+
const done = (code: number | null) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
resolve({ stdout: truncate(stdout), stderr: truncate(stderr), code, timedOut });
|
|
34
|
+
};
|
|
35
|
+
child.on("error", (err) => {
|
|
36
|
+
stderr += String(err.message);
|
|
37
|
+
done(null);
|
|
38
|
+
});
|
|
39
|
+
child.on("close", done);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function truncate(s: string): string {
|
|
44
|
+
s = s.replace(/\n$/, "");
|
|
45
|
+
return s.length > MAX_CHARS ? `${s.slice(0, MAX_CHARS)}\n… [output truncated]` : s;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The transcript format the CLI's own bash mode uses. */
|
|
49
|
+
export function bangMessage(command: string, r: BangResult): string {
|
|
50
|
+
let stderr = r.stderr;
|
|
51
|
+
if (r.timedOut) stderr += `${stderr ? "\n" : ""}[timed out after ${TIMEOUT_MS / 1000}s]`;
|
|
52
|
+
else if (r.code) stderr += `${stderr ? "\n" : ""}[exit code ${r.code}]`;
|
|
53
|
+
return `<bash-input>${command}</bash-input>\n<bash-stdout>${r.stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`;
|
|
54
|
+
}
|