@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,19 @@
|
|
|
1
|
+
/** Adapted from Codex's side-conversation boundary and developer instructions.
|
|
2
|
+
* Our branches are persistent, and writes require the host's explicit switch. */
|
|
3
|
+
export const BTW_BOUNDARY = `Side conversation boundary.
|
|
4
|
+
|
|
5
|
+
Everything before this boundary is inherited history from the parent conversation. It is reference context only, not your current task.
|
|
6
|
+
Do not continue any task, goal, plan, approval, edit or tool call from that history. Only messages submitted after this boundary are active instructions for this conversation.
|
|
7
|
+
You are now in a btw side conversation. Answer the user's side questions and do lightweight, non-mutating exploration. Do not resume the main conversation's work or interact with its sub-agents.
|
|
8
|
+
This side conversation starts read-only. Do not modify files, git state, configuration, permissions or external systems. If the user wants execution, ask them to promote this conversation and explicitly enable writes.
|
|
9
|
+
The host supplies the current conversation role and permissions before each subsequent prompt. Follow that current context if this conversation is later promoted.`;
|
|
10
|
+
|
|
11
|
+
export function conversationInstructions(side: boolean, promoted: boolean, writeEnabled: boolean): string | undefined {
|
|
12
|
+
const role = side
|
|
13
|
+
? `You are in a btw side conversation, separate from the main conversation. Answer only the user's side questions and perform lightweight exploration. Inherited history is reference-only: do not continue its tasks, goals, plans, approvals, edits or tool calls. Do not create or interact with sub-agents, including agents seen in inherited history. Wait for a question rather than continuing the main task.`
|
|
14
|
+
: promoted ? `The host has promoted this former btw side conversation to an ordinary conversation. Its earlier side-conversation role restrictions no longer apply. Continue only the user's current requests here; inherited pre-boundary tasks are still reference-only. Promotion does not itself enable writes.` : "";
|
|
15
|
+
const permissions = writeEnabled
|
|
16
|
+
? side ? "Writes are enabled by the user, but mutations still require an explicit request in this side conversation. Never resume work merely because it appears in inherited history." : ""
|
|
17
|
+
: "The host has disabled writes for this conversation. Use only available read-only tools. Do not execute shell commands, modify workspace or external state, schedule work, or bypass these restrictions through external tools or delegation. Ask the user to turn off read-only before performing a requested mutation.";
|
|
18
|
+
return [role, permissions].filter(Boolean).join("\n\n") || undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { db } from "./db.ts";
|
|
2
|
+
import { sessionGoalProjection } from "./session-file.ts";
|
|
3
|
+
|
|
4
|
+
/** Isolation belongs to a selected native branch, never a permanent session
|
|
5
|
+
* boolean. Rewind automatically discards evidence not present in that branch. */
|
|
6
|
+
export async function needsInheritedGoalCleanup(cwd: string, sessionId: string): Promise<boolean> {
|
|
7
|
+
// Ordinary sessions do not need to open/index a transcript for this policy.
|
|
8
|
+
const origin = db.prepare("SELECT 1 FROM session_view WHERE session_id=? AND (btw_parent IS NOT NULL OR btw_prompt_uuid IS NOT NULL OR btw_question IS NOT NULL)").get(sessionId);
|
|
9
|
+
if (!origin) return false;
|
|
10
|
+
return !(await sessionGoalProjection(cwd, sessionId)).isolated;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Persist only the native identity acknowledged as cleared, not goal state.
|
|
14
|
+
* Branch membership is verified again before trusting this evidence. */
|
|
15
|
+
export function markGoalIsolated(sessionId: string, uuid: string) {
|
|
16
|
+
db.prepare("UPDATE session_view SET btw_goal_isolated=1, btw_goal_isolation_uuid=? WHERE session_id=?").run(uuid, sessionId);
|
|
17
|
+
}
|
package/server/btw.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { historyCwd, setSessionCwd } from "./session-dirs.ts";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { forkSession, getSessionInfo, getSessionMessages } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import type { BtwInfo, BtwItem } from "../shared/protocol.ts";
|
|
5
|
+
import { db } from "./db.ts";
|
|
6
|
+
import { HttpError, sessions } from "./sessions.ts";
|
|
7
|
+
import { BTW_BOUNDARY } from "./btw-instructions.ts";
|
|
8
|
+
import { historyPage } from "./history-pages.ts";
|
|
9
|
+
import { sessionFile } from "./session-file.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* /btw (decided design, see PLAN.md): fork the current session and ask the side question in
|
|
13
|
+
* the fork, so the main thread is never interrupted. The fork is tied to its parent in sqlite
|
|
14
|
+
* (`session_view.btw_parent`) and hidden from the session list until promoted.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type Block = { type: string; text?: string; id?: string; tool_use_id?: string };
|
|
18
|
+
type Msg = { type: string; uuid: string; message?: { content?: unknown } };
|
|
19
|
+
|
|
20
|
+
// The slot belongs to the parent, not to a browser tab or an HTTP request.
|
|
21
|
+
// Serialize lookup + fork so concurrent clients cannot both create a branch.
|
|
22
|
+
const pending = new Map<string, Promise<unknown>>();
|
|
23
|
+
|
|
24
|
+
function blocks(m: Msg): Block[] {
|
|
25
|
+
const c = m.message?.content;
|
|
26
|
+
return Array.isArray(c) ? (c as Block[]) : typeof c === "string" ? [{ type: "text", text: c }] : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The last point where the transcript is a complete conversation: a user message or an
|
|
31
|
+
* assistant text message after which no tool call is left without its result. Forking there is safe while the parent
|
|
32
|
+
* is mid-turn (its last records may be a dangling tool_use).
|
|
33
|
+
*/
|
|
34
|
+
function safeCutPoint(msgs: Msg[]): string | undefined {
|
|
35
|
+
const open = new Set<string>();
|
|
36
|
+
let cut: string | undefined;
|
|
37
|
+
for (const m of msgs) {
|
|
38
|
+
for (const b of blocks(m)) {
|
|
39
|
+
if (b.type === "tool_use" && b.id) open.add(b.id);
|
|
40
|
+
if (b.type === "tool_result" && b.tool_use_id) open.delete(b.tool_use_id);
|
|
41
|
+
}
|
|
42
|
+
if (open.size > 0) continue;
|
|
43
|
+
if (m.type === "user" || blocks(m).some((b) => b.type === "text" && b.text?.trim())) cut = m.uuid;
|
|
44
|
+
}
|
|
45
|
+
return cut;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function askBtw(projectPath: string, parentId: string, question: string): Promise<{ id: string }> {
|
|
49
|
+
// Key by the requested session itself. Its role may change while a request
|
|
50
|
+
// waits (promotion), but requests that could fork from it still share this lock.
|
|
51
|
+
const previous = pending.get(parentId) ?? Promise.resolve();
|
|
52
|
+
const next = previous.catch(() => {}).then(() => askBtwLocked(projectPath, parentId, question));
|
|
53
|
+
pending.set(parentId, next);
|
|
54
|
+
try { return await next; }
|
|
55
|
+
finally { if (pending.get(parentId) === next) pending.delete(parentId); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function askBtwLocked(projectPath: string, parentId: string, question: string): Promise<{ id: string }> {
|
|
59
|
+
question = question.trim();
|
|
60
|
+
const origin = db.prepare("SELECT btw_parent FROM session_view WHERE session_id=?").get(parentId) as { btw_parent: string | null } | undefined;
|
|
61
|
+
// Inside a branch /btw means this same conversation, never a nested fork.
|
|
62
|
+
// Older installations may have several branches; reuse the newest rather than
|
|
63
|
+
// deleting, promoting, or restarting any of the user's existing conversations.
|
|
64
|
+
const existing = origin?.btw_parent ? { session_id: parentId }
|
|
65
|
+
: db.prepare("SELECT session_id FROM session_view WHERE btw_parent=? ORDER BY rowid DESC LIMIT 1").get(parentId) as { session_id: string } | undefined;
|
|
66
|
+
if (existing) {
|
|
67
|
+
if (question) {
|
|
68
|
+
const live = await sessions.getOrCreate(existing.session_id, projectPath);
|
|
69
|
+
if (!await live.send(crypto.randomUUID(), question)) throw new HttpError(409, "旁支消息未能发送,请重试。");
|
|
70
|
+
}
|
|
71
|
+
return { id: existing.session_id };
|
|
72
|
+
}
|
|
73
|
+
if (!question) throw new HttpError(400, "问题不能为空");
|
|
74
|
+
const parentLive = sessions.get(parentId);
|
|
75
|
+
// The parent may run in a worktree of the project: fork next to it and run there.
|
|
76
|
+
const dir = await historyCwd(projectPath, parentId);
|
|
77
|
+
const info = await getSessionInfo(parentId, { dir }).catch(() => undefined);
|
|
78
|
+
const title = `btw: ${question.replace(/\s+/g, " ").slice(0, 80)}`;
|
|
79
|
+
let id: string;
|
|
80
|
+
if (info) {
|
|
81
|
+
const running = parentLive && parentLive.state !== "idle";
|
|
82
|
+
const upToMessageId = running ? safeCutPoint((await getSessionMessages(parentId, { dir })) as Msg[]) : undefined;
|
|
83
|
+
({ sessionId: id } = await forkSession(parentId, { dir, title, ...(upToMessageId ? { upToMessageId } : {}) }));
|
|
84
|
+
} else {
|
|
85
|
+
// Nothing written yet: the side question starts from an empty conversation.
|
|
86
|
+
id = crypto.randomUUID();
|
|
87
|
+
}
|
|
88
|
+
if (dir !== projectPath) setSessionCwd(id, dir);
|
|
89
|
+
const promptUuid = crypto.randomUUID();
|
|
90
|
+
db.prepare("INSERT OR REPLACE INTO session_view (session_id, project_path, btw_parent, btw_question, btw_prompt_uuid, write_enabled) VALUES (?, ?, ?, ?, ?, 0)").run(
|
|
91
|
+
id,
|
|
92
|
+
projectPath,
|
|
93
|
+
parentId,
|
|
94
|
+
question,
|
|
95
|
+
promptUuid,
|
|
96
|
+
);
|
|
97
|
+
const live = await sessions.getOrCreate(id, projectPath);
|
|
98
|
+
if (parentLive?.model) await live.setModel(parentLive.model);
|
|
99
|
+
if (parentLive?.effort) await live.setEffort(parentLive.effort);
|
|
100
|
+
if (!await live.send(promptUuid, question, [], BTW_BOUNDARY)) throw new HttpError(409, "旁支消息未能发送,请重试。");
|
|
101
|
+
sessions.notifyChanged(projectPath);
|
|
102
|
+
return { id };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function btwInfo(projectPath: string, sessionId: string): Promise<BtwInfo> {
|
|
106
|
+
const parentRow = db.prepare("SELECT btw_parent FROM session_view WHERE session_id = ?").get(sessionId) as { btw_parent: string | null } | undefined;
|
|
107
|
+
const rows = db
|
|
108
|
+
.prepare("SELECT session_id, btw_question, btw_prompt_uuid, rewind_anchor FROM session_view WHERE btw_parent = ? AND project_path = ?")
|
|
109
|
+
.all(sessionId, projectPath) as Row[];
|
|
110
|
+
const children = await Promise.all(rows.map((r) => btwItem(projectPath, r)));
|
|
111
|
+
children.sort((a, b) => b.lastModified - a.lastModified);
|
|
112
|
+
return { parent: parentRow?.btw_parent ?? null, children };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type Row = { session_id: string; btw_question: string | null; btw_prompt_uuid: string | null; rewind_anchor: string | null };
|
|
116
|
+
|
|
117
|
+
async function btwItem(projectPath: string, row: Row): Promise<BtwItem> {
|
|
118
|
+
const id = row.session_id;
|
|
119
|
+
const live = sessions.get(id);
|
|
120
|
+
const state = live?.state ?? "idle";
|
|
121
|
+
const dir = await historyCwd(projectPath, id);
|
|
122
|
+
const info = await getSessionInfo(id, { dir }).catch(() => undefined);
|
|
123
|
+
// Reuse the bounded, indexed history reader instead of reading every branch's
|
|
124
|
+
// complete inherited transcript every time a list is invalidated.
|
|
125
|
+
const page = row.btw_prompt_uuid ? await historyPage(await sessionFile(dir, id), { after: row.btw_prompt_uuid, anchor: row.rewind_anchor }).catch(() => undefined) : undefined;
|
|
126
|
+
const msgs = (page?.items ?? []) as Msg[];
|
|
127
|
+
// The answer is what follows the prompt that asked the question.
|
|
128
|
+
const question = row.btw_question ?? info?.customTitle?.replace(/^btw: /, "") ?? "";
|
|
129
|
+
const answer =
|
|
130
|
+
page
|
|
131
|
+
? msgs
|
|
132
|
+
.filter((m) => m.type === "assistant")
|
|
133
|
+
.flatMap((m) => blocks(m).filter((b) => b.type === "text" && b.text).map((b) => b.text!))
|
|
134
|
+
.join("\n\n").slice(0, 2000) || undefined
|
|
135
|
+
: undefined;
|
|
136
|
+
return { id, question, state, answer, lastModified: info?.lastModified ?? live?.lastActivity ?? Date.now() };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "转为普通会话": drop the btw tie so the session shows up in the list. */
|
|
140
|
+
export function promoteBtw(projectPath: string, sessionId: string) {
|
|
141
|
+
db.prepare("UPDATE session_view SET btw_parent = NULL WHERE session_id = ?").run(sessionId);
|
|
142
|
+
sessions.notifyChanged(projectPath);
|
|
143
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for processes we start (the CLI, probe queries, `claude` subcommands, `!` commands).
|
|
3
|
+
*
|
|
4
|
+
* When this server itself runs inside a Claude Code session (e.g. started from its terminal),
|
|
5
|
+
* the environment carries that outer session's identity: its session id, messaging socket and
|
|
6
|
+
* token, effort, "child session" markers. Passing those on would make every CLI we start act as
|
|
7
|
+
* a child of that unrelated session. Configuration variables (CLAUDE_CODE_USE_BEDROCK, …) stay.
|
|
8
|
+
*/
|
|
9
|
+
const OUTER_SESSION_VARS = [
|
|
10
|
+
"CLAUDECODE",
|
|
11
|
+
"CLAUDE_PID",
|
|
12
|
+
"CLAUDE_EFFORT",
|
|
13
|
+
"CLAUDE_JOB_DIR",
|
|
14
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
15
|
+
"CLAUDE_CODE_CHILD_SESSION",
|
|
16
|
+
"CLAUDE_CODE_SESSION_ATTENDED",
|
|
17
|
+
"CLAUDE_CODE_MESSAGING_SOCKET",
|
|
18
|
+
"CLAUDE_CODE_MESSAGING_TOKEN",
|
|
19
|
+
"CLAUDE_CODE_EXECPATH",
|
|
20
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
21
|
+
"CLAUDE_CODE_SSE_PORT",
|
|
22
|
+
"AI_AGENT",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function childEnv(extra: Record<string, string | undefined> = {}): Record<string, string | undefined> {
|
|
26
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
27
|
+
for (const k of OUTER_SESSION_VARS) delete env[k];
|
|
28
|
+
return { ...env, ...extra };
|
|
29
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { query, type Query, type SDKControlInitializeResponse, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { claudeEnv, globalStateFile } from "./config/paths.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SDK adapter for the settings pages: control requests answered by the CLI without a
|
|
8
|
+
* model call. Each call starts a probe query whose input never yields, waits for
|
|
9
|
+
* initialize, runs the request(s) and closes the query (the same trick as cli-info.ts).
|
|
10
|
+
*
|
|
11
|
+
* Several of these methods exist at runtime but are not in sdk.d.ts ([隐藏] in NOTES.md);
|
|
12
|
+
* their shapes are typed here so an SDK upgrade only needs this file checked
|
|
13
|
+
* (`spikes/sdk/m6_config_probe.mjs` prints them all).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type MemoryDialog = {
|
|
17
|
+
files: { kind: string; path: string; label: string; description: string; exists: boolean }[];
|
|
18
|
+
folders: { kind: string; path: string; label: string }[];
|
|
19
|
+
memories: { name: string; path: string; description: string | null; modified_ms: number }[];
|
|
20
|
+
auto_memory: { enabled: boolean; toggleable: boolean; status: string };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type HooksListingRaw = {
|
|
24
|
+
events: { name: string; summary: string; supportsMatcher: boolean; hookCount: number }[];
|
|
25
|
+
hooks: { event: string; matcher?: string; source: string; sourceLabel: string; type: string; displayText: string }[];
|
|
26
|
+
eventCatalog: { name: string; summary: string; supportsMatcher: boolean }[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SkillsDialog = {
|
|
30
|
+
skills: { name: string; display_name: string; description: string; source: string; state: string; tokens?: number; locked_by?: string }[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StatusDialog = { sections: { title: string; rows: { label: string; value: string }[] }[] };
|
|
34
|
+
|
|
35
|
+
export type UsageWindow = { utilization: number | null; resets_at: string | null } | null;
|
|
36
|
+
export type UsageReport = {
|
|
37
|
+
subscription_type: string | null;
|
|
38
|
+
rate_limits_available: boolean;
|
|
39
|
+
rate_limits: (Record<string, unknown> & { five_hour: UsageWindow; seven_day: UsageWindow }) | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type McpStatusEntry = {
|
|
43
|
+
name: string;
|
|
44
|
+
status: string;
|
|
45
|
+
error?: string | null;
|
|
46
|
+
config?: Record<string, unknown>;
|
|
47
|
+
scope?: string;
|
|
48
|
+
source?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type McpAuthResult = {
|
|
52
|
+
authUrl?: string;
|
|
53
|
+
requiresUserAction?: boolean;
|
|
54
|
+
callbackExpected?: boolean;
|
|
55
|
+
callbackPort?: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** The probe query with the hidden methods typed. */
|
|
59
|
+
export type ControlQuery = Query & {
|
|
60
|
+
getMemoryDialog(): Promise<MemoryDialog>;
|
|
61
|
+
getHooksListing(): Promise<HooksListingRaw>;
|
|
62
|
+
getSkillsDialog(): Promise<SkillsDialog>;
|
|
63
|
+
getSettings(): Promise<{ effective: Record<string, unknown>; sources: { source: string; settings: Record<string, unknown> }[] }>;
|
|
64
|
+
getStatus(): Promise<StatusDialog>;
|
|
65
|
+
mcpAuthenticate(server: string, redirectUri?: string): Promise<McpAuthResult>;
|
|
66
|
+
mcpSubmitOAuthCallbackUrl(server: string, callbackUrl: string): Promise<unknown>;
|
|
67
|
+
mcpClearAuth(server: string): Promise<unknown>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const never: AsyncIterable<SDKUserMessage> = { [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {}) }) };
|
|
71
|
+
|
|
72
|
+
// Each probe is a CLI process (~250 MB); keep at most two alive at once.
|
|
73
|
+
const MAX_PROBES = 2;
|
|
74
|
+
let active = 0;
|
|
75
|
+
const waiting: (() => void)[] = [];
|
|
76
|
+
async function acquire() {
|
|
77
|
+
if (active < MAX_PROBES) {
|
|
78
|
+
active++;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await new Promise<void>((r) => waiting.push(r));
|
|
82
|
+
active++;
|
|
83
|
+
}
|
|
84
|
+
function release() {
|
|
85
|
+
active--;
|
|
86
|
+
waiting.shift()?.();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function openProbe(cwd: string): ControlQuery {
|
|
90
|
+
return query({ prompt: never, options: { cwd, settingSources: ["user", "project", "local"], env: claudeEnv() } }) as ControlQuery;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Run control requests against a short-lived probe query in `cwd`. */
|
|
94
|
+
export async function withControl<T>(cwd: string, fn: (q: ControlQuery, init: SDKControlInitializeResponse) => Promise<T>, timeoutMs = 30_000): Promise<T> {
|
|
95
|
+
await acquire();
|
|
96
|
+
const q = openProbe(cwd);
|
|
97
|
+
let timer: NodeJS.Timeout | undefined;
|
|
98
|
+
try {
|
|
99
|
+
const work = (async () => fn(q, await q.initializationResult()))();
|
|
100
|
+
const timeout = new Promise<never>((_, rej) => {
|
|
101
|
+
timer = setTimeout(() => rej(new Error("CLI 控制请求超时")), timeoutMs);
|
|
102
|
+
});
|
|
103
|
+
return await Promise.race([work, timeout]);
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
q.close();
|
|
107
|
+
release();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cache = new Map<string, { at: number; value: Promise<unknown> }>();
|
|
112
|
+
/** Small TTL cache for listings that several requests in a row need (e.g. the memory dialog). */
|
|
113
|
+
export function cached<T>(key: string, ttlMs: number, load: () => Promise<T>): Promise<T> {
|
|
114
|
+
const hit = cache.get(key);
|
|
115
|
+
if (hit && Date.now() - hit.at < ttlMs) return hit.value as Promise<T>;
|
|
116
|
+
const value = load();
|
|
117
|
+
value.catch(() => cache.delete(key));
|
|
118
|
+
cache.set(key, { at: Date.now(), value });
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
export function invalidate(prefix: string) {
|
|
122
|
+
for (const k of cache.keys()) if (k.startsWith(prefix)) cache.delete(k);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const memoryDialog = (cwd: string) => cached(`memory:${cwd}`, 15_000, () => withControl(cwd, (q) => q.getMemoryDialog()));
|
|
126
|
+
export const hooksListing = (cwd: string) => withControl(cwd, (q) => q.getHooksListing());
|
|
127
|
+
export const skillsDialog = (cwd: string) => withControl(cwd, (q) => q.getSkillsDialog());
|
|
128
|
+
export const effectiveSettings = (cwd: string) => withControl(cwd, (q) => q.getSettings());
|
|
129
|
+
export const initInfo = (cwd: string) => cached(`init:${cwd}`, 60_000, () => withControl(cwd, async (_q, init) => init));
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* MCP status. Servers start as "pending" while the probe connects; wait (bounded)
|
|
133
|
+
* until none is pending so the page shows a real connected/failed state.
|
|
134
|
+
*/
|
|
135
|
+
export function mcpStatus(cwd: string, waitMs = 6000): Promise<McpStatusEntry[]> {
|
|
136
|
+
return withControl(cwd, async (q) => {
|
|
137
|
+
const deadline = Date.now() + waitMs;
|
|
138
|
+
for (;;) {
|
|
139
|
+
const list = (await q.mcpServerStatus()) as McpStatusEntry[];
|
|
140
|
+
if (Date.now() > deadline || !list.some((s) => s.status === "pending")) return list;
|
|
141
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function disabledInState(name: string): boolean {
|
|
147
|
+
try {
|
|
148
|
+
const state = JSON.parse(fs.readFileSync(globalStateFile, "utf8")) as { projects?: Record<string, { disabledMcpServers?: string[] }> };
|
|
149
|
+
return Object.values(state.projects ?? {}).some((p) => p.disabledMcpServers?.includes(name));
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Enable/disable an MCP server for this project. The CLI persists it in ~/.claude.json
|
|
157
|
+
* (`projects[<canonical root>].disabledMcpServers`) a moment *after* answering, and
|
|
158
|
+
* enabling a server that then fails to connect rejects ("Connection closed") although
|
|
159
|
+
* the change is still saved [实测]. So wait for the file before closing the probe.
|
|
160
|
+
*/
|
|
161
|
+
export const toggleMcp = (cwd: string, name: string, enabled: boolean) =>
|
|
162
|
+
withControl(cwd, async (q) => {
|
|
163
|
+
let error: Error | undefined;
|
|
164
|
+
try {
|
|
165
|
+
await q.toggleMcpServer(name, enabled);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
error = err as Error;
|
|
168
|
+
}
|
|
169
|
+
const deadline = Date.now() + 4000;
|
|
170
|
+
while (Date.now() < deadline) {
|
|
171
|
+
if (disabledInState(name) === !enabled) return { warning: error?.message };
|
|
172
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
173
|
+
}
|
|
174
|
+
if (error) throw error;
|
|
175
|
+
return {};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const clearMcpAuth = (cwd: string, name: string) => withControl(cwd, (q) => q.mcpClearAuth(name));
|
|
179
|
+
|
|
180
|
+
/** Account facts: initialize response, account, plan usage and /status rows. */
|
|
181
|
+
export function accountSnapshot(cwd: string) {
|
|
182
|
+
return withControl(cwd, async (q, init) => {
|
|
183
|
+
const errors: string[] = [];
|
|
184
|
+
const safe = async <T>(label: string, p: Promise<T>): Promise<T | null> => {
|
|
185
|
+
try {
|
|
186
|
+
return await p;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
errors.push(`${label}: ${(err as Error).message}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const account = await safe("accountInfo", q.accountInfo());
|
|
193
|
+
const usage = await safe("usage", q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true }) as Promise<UsageReport>);
|
|
194
|
+
const status = await safe("getStatus", q.getStatus());
|
|
195
|
+
return { init, account, usage, status, errors };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------- MCP OAuth: the flow state lives in the CLI process, so its probe stays open ----------
|
|
200
|
+
|
|
201
|
+
type AuthFlow = { q: ControlQuery; server: string; cwd: string; timer: NodeJS.Timeout };
|
|
202
|
+
const flows = new Map<string, AuthFlow>();
|
|
203
|
+
const FLOW_TTL_MS = 10 * 60_000;
|
|
204
|
+
|
|
205
|
+
function endFlow(id: string) {
|
|
206
|
+
const f = flows.get(id);
|
|
207
|
+
if (!f) return;
|
|
208
|
+
clearTimeout(f.timer);
|
|
209
|
+
f.q.close();
|
|
210
|
+
flows.delete(id);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Start an MCP OAuth sign-in. Returns the URL to open; the browser ends on the CLI's
|
|
215
|
+
* localhost callback, which a phone cannot reach — so the user pastes that final URL
|
|
216
|
+
* back and we hand it to `mcpSubmitOAuthCallbackUrl`.
|
|
217
|
+
*/
|
|
218
|
+
export async function startMcpAuth(cwd: string, server: string) {
|
|
219
|
+
for (const [id, f] of flows) if (f.server === server && f.cwd === cwd) endFlow(id);
|
|
220
|
+
const q = openProbe(cwd);
|
|
221
|
+
try {
|
|
222
|
+
await q.initializationResult();
|
|
223
|
+
// The server must have been tried once (needs-auth) before authenticating.
|
|
224
|
+
const deadline = Date.now() + 8000;
|
|
225
|
+
while (Date.now() < deadline) {
|
|
226
|
+
const s = ((await q.mcpServerStatus()) as McpStatusEntry[]).find((x) => x.name === server);
|
|
227
|
+
if (!s || s.status !== "pending") break;
|
|
228
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
229
|
+
}
|
|
230
|
+
const res = await q.mcpAuthenticate(server);
|
|
231
|
+
const id = crypto.randomUUID();
|
|
232
|
+
flows.set(id, { q, server, cwd, timer: setTimeout(() => endFlow(id), FLOW_TTL_MS) });
|
|
233
|
+
return { flowId: id, ...res };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
q.close();
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function submitMcpAuth(flowId: string, callbackUrl: string) {
|
|
241
|
+
const f = flows.get(flowId);
|
|
242
|
+
if (!f) throw new Error("登录流程已过期,请重新开始");
|
|
243
|
+
// A rejected URL (state mismatch, no code) leaves the flow waiting, so keep it open on error.
|
|
244
|
+
const res = await f.q.mcpSubmitOAuthCallbackUrl(f.server, callbackUrl);
|
|
245
|
+
endFlow(flowId);
|
|
246
|
+
return res;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function cancelMcpAuth(flowId: string) {
|
|
250
|
+
endFlow(flowId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function closeAllControl() {
|
|
254
|
+
for (const id of [...flows.keys()]) endFlow(id);
|
|
255
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Locations of the CLI's own files (session transcripts live under <claudeDir>/projects). */
|
|
6
|
+
export const claudeDir = () => process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
7
|
+
|
|
8
|
+
/** The CLI's per-project directory name: non-alphanumerics become "-" (long paths get a hash we can't recompute). */
|
|
9
|
+
export function encodedDir(p: string) {
|
|
10
|
+
return p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function exists(p: string, strict = false) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(p);
|
|
16
|
+
return true;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (strict && (err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Directory holding <sessionId>.jsonl, found by name and falling back to a scan. */
|
|
24
|
+
export async function sessionFileDir(projectPath: string, sessionId: string, strict = false): Promise<string | undefined> {
|
|
25
|
+
const projects = path.join(claudeDir(), "projects");
|
|
26
|
+
const direct = path.join(projects, encodedDir(projectPath));
|
|
27
|
+
if (await exists(path.join(direct, `${sessionId}.jsonl`), strict)) return direct;
|
|
28
|
+
if (!(await exists(projects, strict))) return undefined;
|
|
29
|
+
for (const d of await fs.readdir(projects, { withFileTypes: true })) {
|
|
30
|
+
if (d.isDirectory() && (await exists(path.join(projects, d.name, `${sessionId}.jsonl`), strict))) return path.join(projects, d.name);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The first transcript line, where the CLI records session-level facts such as `forkedFrom`. */
|
|
36
|
+
export async function firstRecord(projectPath: string, sessionId: string): Promise<Record<string, unknown> | undefined> {
|
|
37
|
+
const dir = await sessionFileDir(projectPath, sessionId);
|
|
38
|
+
if (!dir) return undefined;
|
|
39
|
+
const fh = await fs.open(path.join(dir, `${sessionId}.jsonl`), "r");
|
|
40
|
+
try {
|
|
41
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
42
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
43
|
+
const line = buf.subarray(0, bytesRead).toString("utf8").split("\n")[0];
|
|
44
|
+
return line ? JSON.parse(line) : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
} finally {
|
|
48
|
+
await fh.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function forkedFrom(projectPath: string, sessionId: string): Promise<{ sessionId: string; messageUuid?: string } | undefined> {
|
|
53
|
+
const f = (await firstRecord(projectPath, sessionId))?.forkedFrom as { sessionId?: string; messageUuid?: string } | undefined;
|
|
54
|
+
return f?.sessionId ? { sessionId: f.sessionId, messageUuid: f.messageUuid } : undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { childEnv } from "./child-env.ts";
|
|
2
|
+
import { query, type SDKControlInitializeResponse, type SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import type { CommandInfo, ModelOption } from "../shared/protocol.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Static CLI facts (models, commands, account, version) without calling the model:
|
|
7
|
+
* start a query whose input never yields, read the init response, close it.
|
|
8
|
+
*/
|
|
9
|
+
const cache = new Map<string, { at: number; value: Promise<SDKControlInitializeResponse> }>();
|
|
10
|
+
const TTL_MS = 10 * 60_000;
|
|
11
|
+
|
|
12
|
+
export function cliInit(cwd: string): Promise<SDKControlInitializeResponse> {
|
|
13
|
+
const hit = cache.get(cwd);
|
|
14
|
+
if (hit && Date.now() - hit.at < TTL_MS) return hit.value;
|
|
15
|
+
const value = (async () => {
|
|
16
|
+
const never: AsyncIterable<SDKUserMessage> = { [Symbol.asyncIterator]: () => ({ next: () => new Promise(() => {}) }) };
|
|
17
|
+
const q = query({ prompt: never, options: { cwd, settingSources: ["user", "project", "local"], env: childEnv() } });
|
|
18
|
+
try {
|
|
19
|
+
return await q.initializationResult();
|
|
20
|
+
} finally {
|
|
21
|
+
q.close();
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
value.catch(() => cache.delete(cwd));
|
|
25
|
+
cache.set(cwd, { at: Date.now(), value });
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function modelOptions(cwd: string): Promise<ModelOption[]> {
|
|
30
|
+
const init = await cliInit(cwd);
|
|
31
|
+
return init.models.map((m) => ({
|
|
32
|
+
value: m.value,
|
|
33
|
+
displayName: m.displayName,
|
|
34
|
+
description: m.description,
|
|
35
|
+
efforts: m.supportedEffortLevels,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------- slash commands for the composer's `/` menu ----------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Commands whose UX is bound to the terminal. The CLI announces them in system/init's
|
|
43
|
+
* `terminal_slash_commands` (only sent when a turn starts), so start from the last observed
|
|
44
|
+
* list [实测 2.1.296] and keep it updated from live sessions.
|
|
45
|
+
*/
|
|
46
|
+
const terminalCommands = new Set(["doctor", "color", "focus", "reload-plugins"]);
|
|
47
|
+
|
|
48
|
+
export function noteTerminalCommands(list: string[] | undefined) {
|
|
49
|
+
for (const c of list ?? []) terminalCommands.add(c);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Commands the web UI implements itself (the CLI has no SDK version of them). */
|
|
53
|
+
const WEB_COMMANDS: CommandInfo[] = [
|
|
54
|
+
{ name: "btw", description: "顺口问一句:从当前会话分出一个旁支会话来回答,不打断主线", argumentHint: "<问题>" },
|
|
55
|
+
{ name: "clear", description: "清空重开:新建一个会话并跳转过去(原会话保留)", argumentHint: "" },
|
|
56
|
+
{ name: "add-dir", description: "允许 Claude 访问项目以外的一个目录(本会话)", argumentHint: "<绝对路径>" },
|
|
57
|
+
{ name: "cd", description: "把当前会话移到另一个目录继续(该目录会加入项目列表)", argumentHint: "<绝对路径>" },
|
|
58
|
+
{ name: "feedback", description: "向 Anthropic 反馈 Claude Code 的问题(附带本会话记录)", argumentHint: "<内容>", aliases: ["bug"] },
|
|
59
|
+
{ name: "release-notes", description: "查看 Claude Code 的版本更新说明", argumentHint: "" },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export async function commandList(cwd: string): Promise<CommandInfo[]> {
|
|
63
|
+
const init = await cliInit(cwd);
|
|
64
|
+
const web = new Set(WEB_COMMANDS.map((c) => c.name));
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
const out: CommandInfo[] = [...WEB_COMMANDS];
|
|
67
|
+
for (const c of init.commands) {
|
|
68
|
+
if (c.name.startsWith("__") || terminalCommands.has(c.name) || web.has(c.name) || seen.has(c.name)) continue;
|
|
69
|
+
seen.add(c.name);
|
|
70
|
+
out.push({ name: c.name, description: c.description, argumentHint: c.argumentHint, ...(c.aliases?.length ? { aliases: c.aliases } : {}) });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import type { AccountStatus, RateWindow } from "../../shared/config.ts";
|
|
3
|
+
import { accountSnapshot, type UsageWindow } from "../cli-control.ts";
|
|
4
|
+
import { configDir, parseJsonOutput, runClaude, tildify } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Account and health (/usage, /status, /doctor). Login state comes from
|
|
8
|
+
* `claude auth status --json`; the web never signs in on the user's behalf.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const win = (w: UsageWindow | undefined): RateWindow | null => (w ? { utilization: w.utilization ?? null, resetsAt: w.resets_at ?? null } : null);
|
|
12
|
+
|
|
13
|
+
const EXTRA_WINDOWS: Record<string, string> = { seven_day_opus: "7 天(Opus)", seven_day_sonnet: "7 天(Sonnet)" };
|
|
14
|
+
|
|
15
|
+
export async function accountStatus(): Promise<AccountStatus> {
|
|
16
|
+
const errors: string[] = [];
|
|
17
|
+
const cwd = os.homedir();
|
|
18
|
+
const [authRun, snap] = await Promise.all([
|
|
19
|
+
runClaude(["auth", "status", "--json"], cwd, { timeoutMs: 30_000 }).catch((err: Error) => {
|
|
20
|
+
errors.push(`auth status: ${err.message}`);
|
|
21
|
+
return null;
|
|
22
|
+
}),
|
|
23
|
+
accountSnapshot(cwd).catch((err: Error) => {
|
|
24
|
+
errors.push(`CLI: ${err.message}`);
|
|
25
|
+
return null;
|
|
26
|
+
}),
|
|
27
|
+
]);
|
|
28
|
+
const raw = authRun ? parseJsonOutput<Record<string, unknown>>(authRun.stdout) : undefined;
|
|
29
|
+
const auth = raw
|
|
30
|
+
? {
|
|
31
|
+
loggedIn: raw.loggedIn === true,
|
|
32
|
+
authMethod: raw.authMethod as string | undefined,
|
|
33
|
+
email: raw.email as string | undefined,
|
|
34
|
+
orgName: raw.orgName as string | undefined,
|
|
35
|
+
subscriptionType: raw.subscriptionType as string | undefined,
|
|
36
|
+
raw,
|
|
37
|
+
}
|
|
38
|
+
: null;
|
|
39
|
+
if (snap) errors.push(...snap.errors);
|
|
40
|
+
const rl = snap?.usage?.rate_limits;
|
|
41
|
+
const extra = rl
|
|
42
|
+
? Object.entries(EXTRA_WINDOWS)
|
|
43
|
+
.map(([k, label]) => ({ label, w: rl[k] as UsageWindow | undefined }))
|
|
44
|
+
.filter((x) => x.w && typeof x.w === "object")
|
|
45
|
+
.map((x) => ({ label: x.label, utilization: x.w!.utilization ?? null, resetsAt: x.w!.resets_at ?? null }))
|
|
46
|
+
: [];
|
|
47
|
+
return {
|
|
48
|
+
auth,
|
|
49
|
+
account: (snap?.account as Record<string, unknown> | null) ?? null,
|
|
50
|
+
version: snap?.init.claude_code_version ?? null,
|
|
51
|
+
model: snap?.status?.sections.flatMap((s) => s.rows).find((r) => r.label === "Model")?.value ?? null,
|
|
52
|
+
usage: rl ? { fiveHour: win(rl.five_hour), sevenDay: win(rl.seven_day), extra } : null,
|
|
53
|
+
status: snap?.status?.sections ?? [],
|
|
54
|
+
configDir: tildify(configDir),
|
|
55
|
+
errors,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function doctor(root: string | null) {
|
|
60
|
+
const r = await runClaude(["doctor"], root ?? os.homedir(), { timeoutMs: 60_000 });
|
|
61
|
+
// The output is meant for a terminal; strip ANSI escapes.
|
|
62
|
+
// eslint-disable-next-line no-control-regex
|
|
63
|
+
const text = (r.stdout + (r.stderr ? `\n${r.stderr}` : "")).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").trim();
|
|
64
|
+
return { code: r.code, text };
|
|
65
|
+
}
|