@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.
Files changed (559) hide show
  1. package/bin/claude-ide.js +23 -0
  2. package/package.json +75 -4
  3. package/server/add-dirs.ts +126 -0
  4. package/server/auth.test.ts +148 -0
  5. package/server/auth.ts +235 -0
  6. package/server/bang.ts +54 -0
  7. package/server/btw-instructions.ts +19 -0
  8. package/server/btw-policy.ts +17 -0
  9. package/server/btw.ts +143 -0
  10. package/server/child-env.ts +29 -0
  11. package/server/cli-control.ts +255 -0
  12. package/server/cli-files.ts +55 -0
  13. package/server/cli-info.ts +73 -0
  14. package/server/config/account.ts +65 -0
  15. package/server/config/agents.ts +186 -0
  16. package/server/config/mcp.ts +86 -0
  17. package/server/config/memory.ts +193 -0
  18. package/server/config/paths.ts +156 -0
  19. package/server/config/plugins.ts +204 -0
  20. package/server/config/routes.ts +95 -0
  21. package/server/config/settings.ts +246 -0
  22. package/server/config.ts +46 -0
  23. package/server/db.ts +121 -0
  24. package/server/directories.test.ts +180 -0
  25. package/server/directories.ts +87 -0
  26. package/server/dist-version.ts +69 -0
  27. package/server/durable-tasks.ts +61 -0
  28. package/server/file-transfers.ts +79 -0
  29. package/server/files.test.ts +127 -0
  30. package/server/files.ts +386 -0
  31. package/server/fs-queue.ts +14 -0
  32. package/server/fsutil.ts +144 -0
  33. package/server/git.ts +86 -0
  34. package/server/goal-state.test.ts +64 -0
  35. package/server/goal-state.ts +78 -0
  36. package/server/history-pages.test.ts +619 -0
  37. package/server/history-pages.ts +513 -0
  38. package/server/http-channel-manager.ts +468 -0
  39. package/server/http-channel-pull.test.ts +314 -0
  40. package/server/http-channel-push.test.ts +364 -0
  41. package/server/http-channel-registration.test.ts +291 -0
  42. package/server/http-channel-routes.ts +73 -0
  43. package/server/http-channel-store.ts +142 -0
  44. package/server/http-channel-test-fixture.ts +103 -0
  45. package/server/ide-messages-mcp.test.ts +82 -0
  46. package/server/ide-messages-mcp.ts +47 -0
  47. package/server/idecron-mcp.ts +65 -0
  48. package/server/idecron.test.ts +79 -0
  49. package/server/idecron.ts +271 -0
  50. package/server/index.ts +336 -0
  51. package/server/live-session.test.ts +1126 -0
  52. package/server/live-session.ts +1165 -0
  53. package/server/project-routes.ts +46 -0
  54. package/server/projects.test.ts +28 -0
  55. package/server/push.ts +315 -0
  56. package/server/report-cache.ts +28 -0
  57. package/server/session-aliases.test.ts +57 -0
  58. package/server/session-aliases.ts +50 -0
  59. package/server/session-branch.ts +71 -0
  60. package/server/session-dirs.ts +189 -0
  61. package/server/session-file.test.ts +134 -0
  62. package/server/session-file.ts +113 -0
  63. package/server/session-messaging.test.ts +297 -0
  64. package/server/session-messaging.ts +165 -0
  65. package/server/sessions.ts +638 -0
  66. package/server/subagents.test.ts +43 -0
  67. package/server/subagents.ts +58 -0
  68. package/server/tool-images.test.ts +76 -0
  69. package/server/tool-images.ts +45 -0
  70. package/server/turn-changes.ts +308 -0
  71. package/server/worktree-git.ts +87 -0
  72. package/server/worktrees.ts +138 -0
  73. package/server/ws.test.ts +195 -0
  74. package/server/ws.ts +197 -0
  75. package/shared/agent-model.ts +15 -0
  76. package/shared/agent-tasks.ts +12 -0
  77. package/shared/config.ts +175 -0
  78. package/shared/directories.ts +12 -0
  79. package/shared/files.ts +51 -0
  80. package/shared/http-channels.ts +63 -0
  81. package/shared/message-window.ts +189 -0
  82. package/shared/project.ts +77 -0
  83. package/shared/protocol.ts +203 -0
  84. package/shared/task-types.ts +4 -0
  85. package/shared/todos.ts +40 -0
  86. package/shared/write-policy.ts +9 -0
  87. package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
  88. package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
  89. package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
  90. package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
  91. package/web/dist/assets/Form-BKTwiYyw.js +1 -0
  92. package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
  93. package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
  94. package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
  95. package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
  96. package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
  97. package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
  98. package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
  99. package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
  100. package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
  101. package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
  102. package/web/dist/assets/abap-CLvhMVsD.js +1 -0
  103. package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  104. package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
  105. package/web/dist/assets/ahk-DwADkxyd.js +1 -0
  106. package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  107. package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  108. package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
  109. package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
  110. package/web/dist/assets/apache-U0d_L8uA.js +1 -0
  111. package/web/dist/assets/apex-KFGFyKwG.js +1 -0
  112. package/web/dist/assets/apl-xBchdRhy.js +1 -0
  113. package/web/dist/assets/applescript-CCn79oCD.js +1 -0
  114. package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  115. package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  116. package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  117. package/web/dist/assets/astro-1q6DGuN3.js +1 -0
  118. package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  119. package/web/dist/assets/awk-BWXHIvNe.js +1 -0
  120. package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  121. package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  122. package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  123. package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  124. package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  125. package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
  126. package/web/dist/assets/berry-DKpUyyne.js +1 -0
  127. package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  128. package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  129. package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  130. package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
  131. package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
  132. package/web/dist/assets/c-D9gIxSfx.js +1 -0
  133. package/web/dist/assets/c3-D0apG41V.js +1 -0
  134. package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  135. package/web/dist/assets/cairo-DRAZz75c.js +1 -0
  136. package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  137. package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  138. package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  139. package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  140. package/web/dist/assets/chapel-CuseGurG.js +1 -0
  141. package/web/dist/assets/clarity-CtfBBspg.js +1 -0
  142. package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  143. package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  144. package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
  145. package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  146. package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  147. package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
  148. package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  149. package/web/dist/assets/coq-CGaSfhwR.js +1 -0
  150. package/web/dist/assets/core-BxI4V4-T.js +12 -0
  151. package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
  152. package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
  153. package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
  154. package/web/dist/assets/css-DpFbYmRh.js +1 -0
  155. package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  156. package/web/dist/assets/cue-CE9AQfxI.js +1 -0
  157. package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  158. package/web/dist/assets/d-qD-0Kul2.js +1 -0
  159. package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  160. package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
  161. package/web/dist/assets/dax-BkyTk9wS.js +1 -0
  162. package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  163. package/web/dist/assets/diff-woXpYk--.js +1 -0
  164. package/web/dist/assets/docker-IyjqRm3v.js +1 -0
  165. package/web/dist/assets/dotenv-CuH5my32.js +1 -0
  166. package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  167. package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  168. package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  169. package/web/dist/assets/edge-DXQWmXTB.js +1 -0
  170. package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
  171. package/web/dist/assets/elm-BslFEg8K.js +1 -0
  172. package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
  173. package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
  174. package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
  175. package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
  176. package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  177. package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  178. package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  179. package/web/dist/assets/fish-BJitypiv.js +1 -0
  180. package/web/dist/assets/fluent-C03EYrpw.js +1 -0
  181. package/web/dist/assets/format-DrBC7w0A.js +1 -0
  182. package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
  183. package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  184. package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
  185. package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
  186. package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  187. package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  188. package/web/dist/assets/genie-CV2tkWYe.js +1 -0
  189. package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  190. package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
  191. package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
  192. package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  193. package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  194. package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  195. package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  196. package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  197. package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  198. package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  199. package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  200. package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
  201. package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
  202. package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
  203. package/web/dist/assets/gn-CAXYAgV-.js +1 -0
  204. package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  205. package/web/dist/assets/go-rLFTqkRN.js +1 -0
  206. package/web/dist/assets/graphql-CV_puxbB.js +1 -0
  207. package/web/dist/assets/groovy-CacY0gHj.js +1 -0
  208. package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  209. package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  210. package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  211. package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  212. package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  213. package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  214. package/web/dist/assets/hack-C5GPjiLV.js +1 -0
  215. package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
  216. package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
  217. package/web/dist/assets/haskell-D8IpX4py.js +1 -0
  218. package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  219. package/web/dist/assets/hcl-Dh228itO.js +1 -0
  220. package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
  221. package/web/dist/assets/hjson-CxZEssPk.js +1 -0
  222. package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  223. package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  224. package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  225. package/web/dist/assets/houston-CsvMBhTu.js +1 -0
  226. package/web/dist/assets/html-CmHiEKV9.js +1 -0
  227. package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
  228. package/web/dist/assets/http-DPjjqtcL.js +1 -0
  229. package/web/dist/assets/hurl-AMBpSloO.js +1 -0
  230. package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
  231. package/web/dist/assets/hy-CZbG8q4J.js +1 -0
  232. package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  233. package/web/dist/assets/index-DNPnl4sh.css +2 -0
  234. package/web/dist/assets/index-DQh72lEx.js +14 -0
  235. package/web/dist/assets/ini-B5eOa1yu.js +1 -0
  236. package/web/dist/assets/java-CEM7Tdw_.js +1 -0
  237. package/web/dist/assets/javascript-CU3PLowd.js +1 -0
  238. package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
  239. package/web/dist/assets/jison-BoKsxs5M.js +1 -0
  240. package/web/dist/assets/json-BM2FWsRB.js +1 -0
  241. package/web/dist/assets/json5-BR5RXkoi.js +1 -0
  242. package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  243. package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  244. package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  245. package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  246. package/web/dist/assets/jsx-B5fTecQP.js +1 -0
  247. package/web/dist/assets/julia-DGyDG39_.js +1 -0
  248. package/web/dist/assets/just-B-RyLcSR.js +1 -0
  249. package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  250. package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  251. package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  252. package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  253. package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  254. package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  255. package/web/dist/assets/langs-CciPKqaF.js +1 -0
  256. package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  257. package/web/dist/assets/latex-CV1pQelm.js +1 -0
  258. package/web/dist/assets/lean-CewbzKMR.js +1 -0
  259. package/web/dist/assets/less-DVTAwKKz.js +1 -0
  260. package/web/dist/assets/lib-DlcVMUqT.js +1 -0
  261. package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  262. package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
  263. package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  264. package/web/dist/assets/log-BNLmms1o.js +1 -0
  265. package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  266. package/web/dist/assets/lua-CIPVh0S-.js +1 -0
  267. package/web/dist/assets/luau-VGWVLoqX.js +1 -0
  268. package/web/dist/assets/make-Dixweg8N.js +1 -0
  269. package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  270. package/web/dist/assets/marko-h2h6YUMS.js +1 -0
  271. package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  272. package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  273. package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  274. package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  275. package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  276. package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  277. package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
  278. package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  279. package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  280. package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
  281. package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  282. package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  283. package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  284. package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  285. package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
  286. package/web/dist/assets/moonbit-D9W784E_.js +1 -0
  287. package/web/dist/assets/move-BYnGhZoM.js +1 -0
  288. package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  289. package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
  290. package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  291. package/web/dist/assets/nginx-jWINucNT.js +1 -0
  292. package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  293. package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  294. package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
  295. package/web/dist/assets/nix-Cl_7024t.js +1 -0
  296. package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  297. package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  298. package/web/dist/assets/nushell--0fT3PI1.js +1 -0
  299. package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  300. package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  301. package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  302. package/web/dist/assets/odin-B1RWQWA5.js +1 -0
  303. package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
  304. package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  305. package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  306. package/web/dist/assets/org-ClD5huOa.js +1 -0
  307. package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  308. package/web/dist/assets/perl-BFegxyeJ.js +1 -0
  309. package/web/dist/assets/php-B-qOA3Y4.js +1 -0
  310. package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  311. package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  312. package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  313. package/web/dist/assets/po-BiJDBrnU.js +1 -0
  314. package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  315. package/web/dist/assets/polar-C7UOKdEL.js +1 -0
  316. package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  317. package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  318. package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
  319. package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  320. package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  321. package/web/dist/assets/proto-DB4EqR-F.js +1 -0
  322. package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
  323. package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  324. package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  325. package/web/dist/assets/python-gzcpVVnB.js +1 -0
  326. package/web/dist/assets/qml-CYUsa87k.js +1 -0
  327. package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  328. package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  329. package/web/dist/assets/r-4xJhgbOf.js +1 -0
  330. package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  331. package/web/dist/assets/raku-B3gFvitq.js +1 -0
  332. package/web/dist/assets/razor-BlHaOHx8.js +1 -0
  333. package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  334. package/web/dist/assets/react-B4UGpcWC.js +1 -0
  335. package/web/dist/assets/red-CYlAq2b0.js +1 -0
  336. package/web/dist/assets/reg-CRGYupPL.js +1 -0
  337. package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
  338. package/web/dist/assets/rel-BtDbiS_P.js +1 -0
  339. package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  340. package/web/dist/assets/ron-VUp2lXgN.js +1 -0
  341. package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  342. package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  343. package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  344. package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  345. package/web/dist/assets/rst-zjfguEh7.js +1 -0
  346. package/web/dist/assets/ruby-4oR9waBK.js +1 -0
  347. package/web/dist/assets/rust-DpiIf5JO.js +1 -0
  348. package/web/dist/assets/sas-Do4V_2i9.js +1 -0
  349. package/web/dist/assets/sass-DXrisJhu.js +1 -0
  350. package/web/dist/assets/scala-BiFt_7mu.js +1 -0
  351. package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  352. package/web/dist/assets/scss-CW6JPtag.js +1 -0
  353. package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  354. package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
  355. package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
  356. package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
  357. package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  358. package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  359. package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  360. package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  361. package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  362. package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
  363. package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
  364. package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  365. package/web/dist/assets/soy-DVtw20Yn.js +1 -0
  366. package/web/dist/assets/sparql-C0ia5K60.js +1 -0
  367. package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  368. package/web/dist/assets/sql-DGCM-jag.js +1 -0
  369. package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  370. package/web/dist/assets/stata-DfuMOXna.js +1 -0
  371. package/web/dist/assets/stylus-B6D30XZt.js +1 -0
  372. package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
  373. package/web/dist/assets/svelte-g_uCUem5.js +1 -0
  374. package/web/dist/assets/swift-CyEgAFGc.js +1 -0
  375. package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  376. package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  377. package/web/dist/assets/systemd-BxMlprV5.js +1 -0
  378. package/web/dist/assets/talonscript-CohzipZa.js +1 -0
  379. package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  380. package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  381. package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
  382. package/web/dist/assets/terraform-DswuEJGm.js +1 -0
  383. package/web/dist/assets/tex-mx6xDwuK.js +1 -0
  384. package/web/dist/assets/themes-CNElGaXw.js +1 -0
  385. package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  386. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  387. package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
  388. package/web/dist/assets/tsv-sltzmVWM.js +1 -0
  389. package/web/dist/assets/tsx-z04y7qR9.js +1 -0
  390. package/web/dist/assets/turtle-ByJddavk.js +1 -0
  391. package/web/dist/assets/twig-Bz57G_nl.js +1 -0
  392. package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
  393. package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
  394. package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
  395. package/web/dist/assets/v-DBj4K9d_.js +1 -0
  396. package/web/dist/assets/vala-zf12oZj6.js +1 -0
  397. package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  398. package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  399. package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  400. package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  401. package/web/dist/assets/viml-DvXPmvsu.js +1 -0
  402. package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  403. package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  404. package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  405. package/web/dist/assets/vue-CQgvQytb.js +1 -0
  406. package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
  407. package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
  408. package/web/dist/assets/vyper-CgoNMtux.js +1 -0
  409. package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  410. package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  411. package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  412. package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  413. package/web/dist/assets/wit-DdvCle-K.js +1 -0
  414. package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  415. package/web/dist/assets/xml-B49klYQ3.js +1 -0
  416. package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
  417. package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
  418. package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  419. package/web/dist/assets/zig-CMLA9XwU.js +1 -0
  420. package/web/dist/icons/apple-touch-icon.png +0 -0
  421. package/web/dist/icons/badge-96.png +0 -0
  422. package/web/dist/icons/icon-192.png +0 -0
  423. package/web/dist/icons/icon-512.png +0 -0
  424. package/web/dist/icons/icon.svg +4 -0
  425. package/web/dist/icons/maskable-192.png +0 -0
  426. package/web/dist/icons/maskable-512.png +0 -0
  427. package/web/dist/index.html +30 -0
  428. package/web/dist/manifest.webmanifest +20 -0
  429. package/web/dist/sw.js +176 -0
  430. package/web/dist/version.json +1 -0
  431. package/web/src/App.tsx +97 -0
  432. package/web/src/agent-history-flow.test.ts +191 -0
  433. package/web/src/agent-history-flow.ts +58 -0
  434. package/web/src/agent-owners.ts +30 -0
  435. package/web/src/annotation-selection.test.ts +36 -0
  436. package/web/src/annotation-selection.ts +123 -0
  437. package/web/src/annotations.test.ts +279 -0
  438. package/web/src/annotations.ts +191 -0
  439. package/web/src/api.ts +27 -0
  440. package/web/src/auth-events.ts +11 -0
  441. package/web/src/auth-lifecycle.test.ts +105 -0
  442. package/web/src/clipboard.ts +32 -0
  443. package/web/src/components/ActivityNotifier.tsx +22 -0
  444. package/web/src/components/AnnotationEditor.tsx +135 -0
  445. package/web/src/components/AnnotationSelection.tsx +122 -0
  446. package/web/src/components/Automation.tsx +390 -0
  447. package/web/src/components/CodeView.tsx +121 -0
  448. package/web/src/components/Composer.tsx +552 -0
  449. package/web/src/components/ComposerAnnotations.tsx +45 -0
  450. package/web/src/components/Confirmation.tsx +91 -0
  451. package/web/src/components/CopyButton.tsx +23 -0
  452. package/web/src/components/DiffView.tsx +94 -0
  453. package/web/src/components/DirectoryPicker.tsx +230 -0
  454. package/web/src/components/FileCopyMenu.tsx +58 -0
  455. package/web/src/components/FileEntryActions.tsx +95 -0
  456. package/web/src/components/ImagePreview.tsx +68 -0
  457. package/web/src/components/Items.tsx +608 -0
  458. package/web/src/components/Markdown.tsx +73 -0
  459. package/web/src/components/MessageList.tsx +458 -0
  460. package/web/src/components/Modal.tsx +72 -0
  461. package/web/src/components/NotificationEntry.tsx +31 -0
  462. package/web/src/components/NotificationSettings.tsx +226 -0
  463. package/web/src/components/NotificationToasts.tsx +59 -0
  464. package/web/src/components/OpenTabs.tsx +112 -0
  465. package/web/src/components/Pending.tsx +175 -0
  466. package/web/src/components/ProjectExtras.tsx +299 -0
  467. package/web/src/components/ProjectNav.tsx +47 -0
  468. package/web/src/components/SessionActions.tsx +271 -0
  469. package/web/src/components/SessionChannels.tsx +288 -0
  470. package/web/src/components/SessionExtras.tsx +263 -0
  471. package/web/src/components/SettingsLink.tsx +21 -0
  472. package/web/src/components/Shell.tsx +320 -0
  473. package/web/src/components/SvgPreview.tsx +18 -0
  474. package/web/src/components/TodoPanel.tsx +37 -0
  475. package/web/src/components/VersionNotice.tsx +40 -0
  476. package/web/src/components/icons.tsx +43 -0
  477. package/web/src/diff.ts +62 -0
  478. package/web/src/drafts.test.ts +80 -0
  479. package/web/src/drafts.ts +64 -0
  480. package/web/src/format.ts +5 -0
  481. package/web/src/highlight.worker.ts +49 -0
  482. package/web/src/hooks.ts +59 -0
  483. package/web/src/item-view-state.test.ts +51 -0
  484. package/web/src/item-view-state.ts +43 -0
  485. package/web/src/live-history-flow.test.ts +77 -0
  486. package/web/src/live-merge.test.ts +64 -0
  487. package/web/src/live-merge.ts +50 -0
  488. package/web/src/live-message-hydration.test.ts +253 -0
  489. package/web/src/live.ts +404 -0
  490. package/web/src/main.tsx +27 -0
  491. package/web/src/markdown-assets.test.ts +70 -0
  492. package/web/src/markdown-assets.ts +78 -0
  493. package/web/src/markdown-html.test.ts +84 -0
  494. package/web/src/markdown-html.ts +60 -0
  495. package/web/src/markdown-svg.test.ts +37 -0
  496. package/web/src/markdown-svg.ts +42 -0
  497. package/web/src/markdown-tables.test.ts +34 -0
  498. package/web/src/markdown-tables.ts +17 -0
  499. package/web/src/message-bodies.ts +117 -0
  500. package/web/src/message-cache.test.ts +208 -0
  501. package/web/src/message-cache.ts +158 -0
  502. package/web/src/message-font-size.test.ts +41 -0
  503. package/web/src/message-font-size.ts +57 -0
  504. package/web/src/message-time.test.ts +32 -0
  505. package/web/src/message-time.ts +26 -0
  506. package/web/src/notify.ts +132 -0
  507. package/web/src/open-tabs.test.ts +69 -0
  508. package/web/src/open-tabs.ts +110 -0
  509. package/web/src/pages/AgentView.tsx +138 -0
  510. package/web/src/pages/ChangesView.tsx +193 -0
  511. package/web/src/pages/FilesView.tsx +406 -0
  512. package/web/src/pages/HomePage.tsx +96 -0
  513. package/web/src/pages/LoginPage.tsx +56 -0
  514. package/web/src/pages/OverviewPage.tsx +124 -0
  515. package/web/src/pages/ProjectHome.tsx +136 -0
  516. package/web/src/pages/SessionView.tsx +238 -0
  517. package/web/src/pages/WorktreesView.tsx +197 -0
  518. package/web/src/pages/settings/AccountPage.tsx +149 -0
  519. package/web/src/pages/settings/AgentsPage.tsx +182 -0
  520. package/web/src/pages/settings/ConfigPage.tsx +181 -0
  521. package/web/src/pages/settings/GeneralPage.tsx +27 -0
  522. package/web/src/pages/settings/HooksPage.tsx +244 -0
  523. package/web/src/pages/settings/McpPage.tsx +310 -0
  524. package/web/src/pages/settings/MemoryPage.tsx +309 -0
  525. package/web/src/pages/settings/PermissionsPage.tsx +160 -0
  526. package/web/src/pages/settings/PluginsPage.tsx +251 -0
  527. package/web/src/pages/settings/SettingsPage.tsx +141 -0
  528. package/web/src/pages/settings/SkillsPage.tsx +142 -0
  529. package/web/src/pages/settings/ui.tsx +165 -0
  530. package/web/src/preference-document.ts +133 -0
  531. package/web/src/project-actions.ts +18 -0
  532. package/web/src/project-files-state.test.ts +49 -0
  533. package/web/src/project-files-state.ts +64 -0
  534. package/web/src/project-images.ts +13 -0
  535. package/web/src/pwa.ts +33 -0
  536. package/web/src/queryClient.ts +5 -0
  537. package/web/src/reasoning-preference.test.ts +39 -0
  538. package/web/src/reasoning-preference.ts +42 -0
  539. package/web/src/session-confirmations.ts +36 -0
  540. package/web/src/stream-replay.test.ts +163 -0
  541. package/web/src/styles.css +231 -0
  542. package/web/src/tool-result-body.test.ts +115 -0
  543. package/web/src/transcript.test.ts +186 -0
  544. package/web/src/transcript.ts +583 -0
  545. package/web/src/turns.test.ts +338 -0
  546. package/web/src/turns.ts +178 -0
  547. package/web/src/ui/Button.tsx +60 -0
  548. package/web/src/ui/Controls.tsx +227 -0
  549. package/web/src/ui/Form.tsx +203 -0
  550. package/web/src/ui/MenuPopover.tsx +144 -0
  551. package/web/src/ui/Popover.tsx +147 -0
  552. package/web/src/ui/ResizablePane.tsx +110 -0
  553. package/web/src/ui/Select.tsx +150 -0
  554. package/web/src/ui/icons.tsx +270 -0
  555. package/web/src/unsaved.tsx +99 -0
  556. package/web/src/user-text.test.ts +60 -0
  557. package/web/src/user-text.ts +35 -0
  558. package/web/src/uuid.ts +12 -0
  559. package/README.md +0 -3
@@ -0,0 +1,46 @@
1
+ import type { Hono } from "hono";
2
+ import { HttpError, projectPath } from "./sessions.ts";
3
+ import { turnChanges, turnFileDiff } from "./turn-changes.ts";
4
+ import { addExtraDir, extraDirs, removeExtraDir } from "./add-dirs.ts";
5
+ import { createWorktree, createWorktreeSession, listWorktrees, removeWorktree } from "./worktrees.ts";
6
+
7
+ /** M4 routes: files changed per turn, extra directories (/add-dir), git worktrees. */
8
+ export function registerProjectRoutes(app: Hono) {
9
+ const pp = (c: { req: { param(k: string): string | undefined } }) => projectPath(c.req.param("pid") ?? "");
10
+
11
+ app.get("/api/projects/:pid/sessions/:sid/turn-changes", async (c) => c.json(await turnChanges(pp(c), c.req.param("sid"))));
12
+
13
+ app.get("/api/projects/:pid/sessions/:sid/turn-changes/diff", async (c) => {
14
+ const turn = c.req.query("turn");
15
+ const file = c.req.query("path");
16
+ if (!turn || !file) throw new HttpError(400, "缺少 turn 或 path");
17
+ return c.json(await turnFileDiff(pp(c), c.req.param("sid"), turn, file));
18
+ });
19
+
20
+ app.get("/api/projects/:pid/sessions/:sid/dirs", async (c) => c.json(await extraDirs(pp(c), c.req.param("sid"))));
21
+
22
+ app.post("/api/projects/:pid/sessions/:sid/dirs", async (c) => {
23
+ const body = await c.req.json().catch(() => ({}));
24
+ return c.json(await addExtraDir(pp(c), c.req.param("sid"), String(body?.path ?? ""), !!body?.remember));
25
+ });
26
+
27
+ app.delete("/api/projects/:pid/sessions/:sid/dirs", async (c) => {
28
+ const dir = c.req.query("path");
29
+ if (!dir) throw new HttpError(400, "缺少 path");
30
+ return c.json(await removeExtraDir(pp(c), c.req.param("sid"), dir));
31
+ });
32
+
33
+ app.get("/api/projects/:pid/worktrees", async (c) => c.json(await listWorktrees(pp(c))));
34
+
35
+ app.post("/api/projects/:pid/worktrees", async (c) => {
36
+ const body = await c.req.json().catch(() => ({}));
37
+ const project = pp(c);
38
+ const wt = await createWorktree(project, String(body?.name ?? ""));
39
+ const sessionId = body?.session ? await createWorktreeSession(project, wt.name) : undefined;
40
+ return c.json({ worktree: wt, sessionId });
41
+ });
42
+
43
+ app.post("/api/projects/:pid/worktrees/:name/sessions", async (c) => c.json({ id: await createWorktreeSession(pp(c), c.req.param("name")) }));
44
+
45
+ app.delete("/api/projects/:pid/worktrees/:name", async (c) => c.json(await removeWorktree(pp(c), c.req.param("name"), c.req.query("force") === "1")));
46
+ }
@@ -0,0 +1,28 @@
1
+ import { test, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { channelFixture } from "./http-channel-test-fixture.ts";
6
+
7
+ const fixture = await channelFixture();
8
+ after(() => fixture.close());
9
+ const { addProject, removeProject, listProjects, overview } = await import("./sessions.ts");
10
+
11
+ test("removing a project only unregisters it, preserves files and session state, and hides its overview links", async () => {
12
+ const project = fixture.project;
13
+ const file = path.join(fixture.tmp, "keep.txt");
14
+ fs.writeFileSync(file, "project content");
15
+ const sessionId = fixture.newSession();
16
+ const live = await fixture.sessions.getOrCreate(sessionId, fixture.tmp);
17
+ await live.sendExternalText(fixture.id(), "running", () => true);
18
+ assert.ok((await overview()).some((entry) => entry.sessionId === sessionId));
19
+ const sessionRow = fixture.db.prepare("SELECT * FROM session_view WHERE session_id=?").get(sessionId);
20
+ removeProject(project.id);
21
+ assert.ok(!(await listProjects()).some((entry) => entry.id === project.id));
22
+ assert.equal(fs.readFileSync(file, "utf8"), "project content");
23
+ assert.deepEqual(fixture.db.prepare("SELECT * FROM session_view WHERE session_id=?").get(sessionId), sessionRow);
24
+ assert.equal(fixture.sessions.get(sessionId), live);
25
+ assert.ok(!(await overview()).some((entry) => entry.sessionId === sessionId));
26
+ assert.equal((await addProject(fixture.tmp)).id, project.id);
27
+ assert.ok((await overview()).some((entry) => entry.sessionId === sessionId));
28
+ });
package/server/push.ts ADDED
@@ -0,0 +1,315 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import type { Hono } from "hono";
5
+ import webpush from "web-push";
6
+ import { config } from "./config.ts";
7
+ import { db } from "./db.ts";
8
+
9
+ /**
10
+ * Web Push: VAPID keys live in <dataDir>/vapid.json (generated on first run), subscriptions in
11
+ * sqlite (push_subscriptions). `notify()` is what the rest of the server calls.
12
+ */
13
+
14
+ export type NotifyKind = "done" | "attention" | "error";
15
+ export const NOTIFY_KINDS: NotifyKind[] = ["done", "attention", "error"];
16
+ export type NotifyKinds = Record<NotifyKind, boolean>;
17
+
18
+ export type NotifyInput = {
19
+ kind: NotifyKind;
20
+ title: string;
21
+ body?: string;
22
+ /** Same-origin path to open when the notification is clicked, e.g. /p/<pid>/s/<sid>. */
23
+ url?: string;
24
+ /** Notifications with the same tag replace each other on the device and are deduplicated here. */
25
+ tag?: string;
26
+ /**
27
+ * Session id: skip the push when some device is currently looking at this session.
28
+ * Only effective once a checker is installed with setViewingCheck().
29
+ */
30
+ skipIfViewing?: string;
31
+ };
32
+
33
+ export type NotifyResult = { sent: number; removed: number; failed: number; skipped?: "dedup" | "viewing" | "none" };
34
+
35
+ /** The payload the service worker (web/sw.js) receives. */
36
+ export type PushPayload = { kind: NotifyKind | "test"; title: string; body: string; url: string; tag?: string };
37
+
38
+ // ---------- VAPID ----------
39
+
40
+ type Vapid = { publicKey: string; privateKey: string };
41
+
42
+ function loadVapid(): Vapid {
43
+ const file = path.join(config.dataDir, "vapid.json");
44
+ try {
45
+ const v = JSON.parse(fs.readFileSync(file, "utf8")) as Vapid;
46
+ if (v.publicKey && v.privateKey) return v;
47
+ } catch {
48
+ // generate below
49
+ }
50
+ const keys = webpush.generateVAPIDKeys();
51
+ fs.writeFileSync(file, JSON.stringify(keys, null, 2) + "\n", { mode: 0o600 });
52
+ return keys;
53
+ }
54
+
55
+ const vapid = loadVapid();
56
+ /** Must be a mailto: or https: URL; Apple's push service rejects obviously fake ones (e.g. localhost). */
57
+ const vapidSubject = process.env.CLAUDE_IDE_VAPID_SUBJECT ?? "mailto:claude-ide@example.com";
58
+
59
+ export function vapidPublicKey(): string {
60
+ return vapid.publicKey;
61
+ }
62
+
63
+ // ---------- storage ----------
64
+
65
+ type Row = {
66
+ id: string;
67
+ endpoint: string;
68
+ p256dh: string;
69
+ auth: string;
70
+ device: string | null;
71
+ user_agent: string | null;
72
+ created_at: number;
73
+ last_success_at: number | null;
74
+ notify_done: number;
75
+ notify_attention: number;
76
+ notify_error: number;
77
+ };
78
+
79
+ export type SubscriptionInfo = {
80
+ id: string;
81
+ device: string;
82
+ userAgent: string;
83
+ createdAt: number;
84
+ lastSuccessAt: number | null;
85
+ kinds: NotifyKinds;
86
+ };
87
+
88
+ const idOf = (endpoint: string) => crypto.createHash("sha256").update(endpoint).digest("base64url").slice(0, 16);
89
+
90
+ function kindsOf(r: Row): NotifyKinds {
91
+ return { done: !!r.notify_done, attention: !!r.notify_attention, error: !!r.notify_error };
92
+ }
93
+
94
+ function info(r: Row): SubscriptionInfo {
95
+ return {
96
+ id: r.id,
97
+ device: r.device || guessDevice(r.user_agent ?? ""),
98
+ userAgent: r.user_agent ?? "",
99
+ createdAt: r.created_at,
100
+ lastSuccessAt: r.last_success_at,
101
+ kinds: kindsOf(r),
102
+ };
103
+ }
104
+
105
+ function guessDevice(ua: string): string {
106
+ const os = /iPhone/.test(ua) ? "iPhone" : /iPad/.test(ua) ? "iPad" : /Android/.test(ua) ? "Android" : /Mac OS X/.test(ua) ? "Mac" : /Windows/.test(ua) ? "Windows" : /Linux/.test(ua) ? "Linux" : "设备";
107
+ const browser = /Edg\//.test(ua) ? "Edge" : /Firefox\//.test(ua) ? "Firefox" : /Chrome\//.test(ua) ? "Chrome" : /Safari\//.test(ua) ? "Safari" : "";
108
+ return browser ? `${os} · ${browser}` : os;
109
+ }
110
+
111
+ const rowByEndpoint = (endpoint: string) => db.prepare("SELECT * FROM push_subscriptions WHERE endpoint = ?").get(endpoint) as Row | undefined;
112
+ const deleteRow = (id: string) => db.prepare("DELETE FROM push_subscriptions WHERE id = ?").run(id);
113
+
114
+ export function listSubscriptions(): SubscriptionInfo[] {
115
+ return (db.prepare("SELECT * FROM push_subscriptions ORDER BY created_at").all() as Row[]).map(info);
116
+ }
117
+
118
+ /**
119
+ * Push services are https. Plain http is accepted only for loopback endpoints, which lets the e2e
120
+ * tests stand in for a push service without touching the network.
121
+ */
122
+ function validEndpoint(endpoint: unknown): endpoint is string {
123
+ if (typeof endpoint !== "string" || endpoint.length > 2048) return false;
124
+ try {
125
+ const u = new URL(endpoint);
126
+ if (u.protocol === "https:") return true;
127
+ return u.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ function parseKinds(v: unknown): Partial<NotifyKinds> {
134
+ const out: Partial<NotifyKinds> = {};
135
+ if (v && typeof v === "object") {
136
+ for (const k of NOTIFY_KINDS) {
137
+ const b = (v as Record<string, unknown>)[k];
138
+ if (typeof b === "boolean") out[k] = b;
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+
144
+ /** Upserts a subscription; undefined when the body is not a valid PushSubscription. */
145
+ export function saveSubscription(body: unknown, userAgent: string): SubscriptionInfo | undefined {
146
+ const b = (body ?? {}) as { subscription?: { endpoint?: unknown; keys?: { p256dh?: unknown; auth?: unknown } }; device?: unknown; kinds?: unknown };
147
+ const sub = b.subscription;
148
+ const endpoint = sub?.endpoint;
149
+ const p256dh = sub?.keys?.p256dh;
150
+ const auth = sub?.keys?.auth;
151
+ if (!validEndpoint(endpoint) || typeof p256dh !== "string" || typeof auth !== "string" || !p256dh || !auth) {
152
+ return undefined;
153
+ }
154
+ try {
155
+ // Encrypting a dummy payload validates the keys (p256dh must be a P-256 point, auth 16 bytes).
156
+ webpush.encrypt(p256dh, auth, "x", "aes128gcm");
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ const device = typeof b.device === "string" ? b.device.slice(0, 80) : null;
161
+ const kinds = { done: true, attention: true, error: true, ...parseKinds(b.kinds) };
162
+ const existing = rowByEndpoint(endpoint);
163
+ if (existing) {
164
+ const k = { ...kindsOf(existing), ...parseKinds(b.kinds) };
165
+ db.prepare(
166
+ "UPDATE push_subscriptions SET p256dh = ?, auth = ?, device = COALESCE(?, device), user_agent = ?, notify_done = ?, notify_attention = ?, notify_error = ? WHERE id = ?",
167
+ ).run(p256dh, auth, device, userAgent, +k.done, +k.attention, +k.error, existing.id);
168
+ } else {
169
+ db.prepare(
170
+ `INSERT INTO push_subscriptions (id, endpoint, p256dh, auth, device, user_agent, created_at, notify_done, notify_attention, notify_error)
171
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
172
+ ).run(idOf(endpoint), endpoint, p256dh, auth, device, userAgent, Date.now(), +kinds.done, +kinds.attention, +kinds.error);
173
+ }
174
+ return info(rowByEndpoint(endpoint)!);
175
+ }
176
+
177
+ function findSub(body: { endpoint?: unknown; id?: unknown }): Row | undefined {
178
+ if (typeof body.endpoint === "string") return rowByEndpoint(body.endpoint);
179
+ if (typeof body.id === "string") return db.prepare("SELECT * FROM push_subscriptions WHERE id = ?").get(body.id) as Row | undefined;
180
+ return undefined;
181
+ }
182
+
183
+ function updateKinds(row: Row, kinds: Partial<NotifyKinds>): SubscriptionInfo {
184
+ const k = { ...kindsOf(row), ...kinds };
185
+ db.prepare("UPDATE push_subscriptions SET notify_done = ?, notify_attention = ?, notify_error = ? WHERE id = ?").run(+k.done, +k.attention, +k.error, row.id);
186
+ return info({ ...row, notify_done: +k.done, notify_attention: +k.attention, notify_error: +k.error });
187
+ }
188
+
189
+ // ---------- sending ----------
190
+
191
+ type SendOutcome = { ok: true } | { ok: false; gone: boolean; status?: number; message: string };
192
+
193
+ async function sendTo(row: Row, payload: PushPayload, urgency: "normal" | "high"): Promise<SendOutcome> {
194
+ let req;
195
+ try {
196
+ req = webpush.generateRequestDetails({ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } }, JSON.stringify(payload), {
197
+ vapidDetails: { subject: vapidSubject, publicKey: vapid.publicKey, privateKey: vapid.privateKey },
198
+ TTL: 60 * 60,
199
+ urgency,
200
+ // Topic collapses still-undelivered messages with the same tag on the push service.
201
+ ...(payload.tag ? { topic: crypto.createHash("sha256").update(payload.tag).digest("base64url").slice(0, 32) } : {}),
202
+ });
203
+ } catch (err) {
204
+ // Malformed keys can never work: treat like an expired subscription.
205
+ return { ok: false, gone: true, message: (err as Error).message };
206
+ }
207
+ try {
208
+ // web-push's own sender only speaks https; fetch lets loopback test endpoints work too.
209
+ const res = await fetch(req.endpoint, { method: req.method, headers: req.headers, body: new Uint8Array(req.body), signal: AbortSignal.timeout(15_000) });
210
+ if (res.ok) {
211
+ db.prepare("UPDATE push_subscriptions SET last_success_at = ? WHERE id = ?").run(Date.now(), row.id);
212
+ return { ok: true };
213
+ }
214
+ const text = (await res.text().catch(() => "")).slice(0, 200);
215
+ const gone = res.status === 404 || res.status === 410;
216
+ return { ok: false, gone, status: res.status, message: `推送服务返回 ${res.status}${text ? `:${text}` : ""}` };
217
+ } catch (err) {
218
+ return { ok: false, gone: false, message: `无法连接推送服务:${(err as Error).message}` };
219
+ }
220
+ }
221
+
222
+ let viewingCheck: ((sessionId: string) => boolean) | undefined;
223
+
224
+ /** Install the "is any device looking at this session right now?" check used by skipIfViewing. */
225
+ export function setViewingCheck(fn: ((sessionId: string) => boolean) | undefined): void {
226
+ viewingCheck = fn;
227
+ }
228
+
229
+ const DEDUP_MS = 10_000;
230
+ const recentTags = new Map<string, number>();
231
+
232
+ /**
233
+ * Push a notification to every subscribed device that wants this kind.
234
+ * Delivery failures never throw; expired subscriptions (404/410) are removed.
235
+ */
236
+ export async function notify(input: NotifyInput): Promise<NotifyResult> {
237
+ const now = Date.now();
238
+ if (input.tag) {
239
+ for (const [t, at] of recentTags) if (now - at > DEDUP_MS) recentTags.delete(t);
240
+ if (recentTags.has(input.tag)) return { sent: 0, removed: 0, failed: 0, skipped: "dedup" };
241
+ }
242
+ if (input.skipIfViewing && viewingCheck?.(input.skipIfViewing)) return { sent: 0, removed: 0, failed: 0, skipped: "viewing" };
243
+ if (input.tag) recentTags.set(input.tag, now);
244
+
245
+ if (!NOTIFY_KINDS.includes(input.kind)) throw new Error(`unknown notification kind: ${String(input.kind)}`);
246
+ const column = `notify_${input.kind}`;
247
+ const rows = db.prepare(`SELECT * FROM push_subscriptions WHERE ${column} = 1`).all() as Row[];
248
+ if (rows.length === 0) return { sent: 0, removed: 0, failed: 0, skipped: "none" };
249
+ const payload: PushPayload = { kind: input.kind, title: input.title, body: input.body ?? "", url: input.url ?? "/", tag: input.tag };
250
+ const urgency = input.kind === "done" ? "normal" : "high";
251
+ const outcomes = await Promise.all(rows.map((r) => sendTo(r, payload, urgency)));
252
+ const result: NotifyResult = { sent: 0, removed: 0, failed: 0 };
253
+ outcomes.forEach((o, i) => {
254
+ if (o.ok) result.sent++;
255
+ else if (o.gone) {
256
+ deleteRow(rows[i]!.id);
257
+ result.removed++;
258
+ } else {
259
+ result.failed++;
260
+ console.warn(`push to ${rows[i]!.id} failed: ${o.message}`);
261
+ }
262
+ });
263
+ return result;
264
+ }
265
+
266
+ // ---------- routes ----------
267
+
268
+ const NOT_SUBSCRIBED = "此设备尚未订阅推送";
269
+
270
+ /** Routes under /api/push (behind requireAuth, which index.ts applies to /api/*). */
271
+ export function registerPushRoutes(app: Hono) {
272
+ app.get("/api/push/key", (c) => c.json({ publicKey: vapid.publicKey }));
273
+
274
+ app.get("/api/push/subscriptions", (c) => c.json(listSubscriptions()));
275
+
276
+ /** Settings of one device (by endpoint) — `subscribed: false` when the server doesn't know it. */
277
+ app.get("/api/push/settings", (c) => {
278
+ const row = findSub({ endpoint: c.req.query("endpoint") });
279
+ return c.json(row ? { subscribed: true, ...info(row) } : { subscribed: false });
280
+ });
281
+
282
+ app.patch("/api/push/settings", async (c) => {
283
+ const body = await c.req.json().catch(() => ({}));
284
+ const row = findSub(body);
285
+ if (!row) return c.json({ error: NOT_SUBSCRIBED }, 404);
286
+ return c.json(updateKinds(row, parseKinds(body.kinds)));
287
+ });
288
+
289
+ app.post("/api/push/subscribe", async (c) => {
290
+ const body = await c.req.json().catch(() => ({}));
291
+ const saved = saveSubscription(body, c.req.header("user-agent") ?? "");
292
+ return saved ? c.json(saved) : c.json({ error: "无效的推送订阅" }, 400);
293
+ });
294
+
295
+ app.delete("/api/push/subscribe", async (c) => {
296
+ const body = await c.req.json().catch(() => ({}));
297
+ const row = findSub(body);
298
+ if (row) deleteRow(row.id);
299
+ return c.json({ ok: true, removed: !!row });
300
+ });
301
+
302
+ app.post("/api/push/test", async (c) => {
303
+ const body = await c.req.json().catch(() => ({}));
304
+ if (typeof body.endpoint !== "string" && typeof body.id !== "string") return c.json({ error: "缺少 endpoint" }, 400);
305
+ const row = findSub(body);
306
+ if (!row) return c.json({ error: NOT_SUBSCRIBED }, 404);
307
+ const out = await sendTo(row, { kind: "test", title: "Claude IDE", body: "测试通知:推送已开启。", url: "/", tag: "test" }, "high");
308
+ if (out.ok) return c.json({ ok: true });
309
+ if (out.gone) {
310
+ deleteRow(row.id);
311
+ return c.json({ error: "订阅已失效,已删除;请重新开启推送" }, 410);
312
+ }
313
+ return c.json({ error: out.message }, 502);
314
+ });
315
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The agent SDK detects musl vs glibc with `process.report.getReport()`. That
3
+ * walks every socket and calls getnameinfo() for each endpoint, which on WSL
4
+ * blocks in a synchronous reverse-DNS lookup (5s timeouts) and freezes the
5
+ * event loop. The report is also only inspected for `header.glibcVersionRuntime`,
6
+ * so compute it once at startup (no client sockets yet) and reuse it.
7
+ *
8
+ * CLAUDE_IDE_CACHE_REPORT=0 disables the workaround.
9
+ */
10
+ import fs from "node:fs";
11
+
12
+ const report = process.report as typeof process.report | undefined;
13
+
14
+ if (process.env.CLAUDE_IDE_CACHE_REPORT !== "0" && process.platform === "linux" && typeof report?.getReport === "function") {
15
+ const original = report.getReport.bind(report);
16
+ let cached: ReturnType<typeof original> | undefined;
17
+ try {
18
+ cached = original();
19
+ fs.appendFileSync("/tmp/report-cache.log", `${new Date().toISOString()} cached report at startup pid=${process.pid}\n`);
20
+ } catch {
21
+ // fall through to the original call
22
+ }
23
+ if (cached) {
24
+ // Only the no-argument call (what the SDK makes) is served from the cache;
25
+ // options-bearing calls keep their original semantics.
26
+ report.getReport = ((...args: Parameters<typeof original>) => (args.length ? original(...args) : cached)) as typeof report.getReport;
27
+ }
28
+ }
@@ -0,0 +1,57 @@
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
+
7
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cide-aliases-"));
8
+ process.env.CLAUDE_IDE_DATA_DIR = dir;
9
+ process.env.CLAUDE_IDE_PASSWORD = "synthetic-alias-test";
10
+ const { db } = await import("./db.ts");
11
+ const { aliasesFor, aliasTarget, aliasForTarget, setAlias, removeAlias, removeSessionAliases, validateAlias } = await import("./session-aliases.ts");
12
+ after(async () => { db.close(); await fs.rm(dir, { recursive: true, force: true }); });
13
+
14
+ test("aliases are owner-local, and reverse source lookup uses the receiver's address book", () => {
15
+ setAlias("a", "b", "reviewer");
16
+ setAlias("b", "a", "planner");
17
+ setAlias("c", "d", "reviewer");
18
+ assert.equal(aliasTarget("a", "reviewer"), "b");
19
+ assert.equal(aliasTarget("c", "reviewer"), "d");
20
+ assert.equal(aliasForTarget("b", "a"), "planner");
21
+ assert.equal(aliasForTarget("d", "a"), undefined);
22
+ assert.deepEqual(aliasesFor("a"), [{ alias: "reviewer", sessionId: "b" }]);
23
+ });
24
+
25
+ test("renaming keeps one nickname per target and refuses another contact's name", () => {
26
+ setAlias("rename-owner", "first", "old");
27
+ setAlias("rename-owner", "second", "taken");
28
+ assert.throws(() => setAlias("rename-owner", "first", "taken"), /already refers/);
29
+ assert.equal(aliasTarget("rename-owner", "old"), "first");
30
+ assert.equal(aliasTarget("rename-owner", "taken"), "second");
31
+ setAlias("rename-owner", "first", " new ");
32
+ assert.equal(aliasTarget("rename-owner", "old"), undefined);
33
+ assert.equal(aliasTarget("rename-owner", "new"), "first");
34
+ setAlias("rename-owner", "first", "new");
35
+ assert.equal(aliasesFor("rename-owner").length, 2);
36
+ });
37
+
38
+ test("removal is scoped to owner; actual deletion clears incoming and outgoing aliases only", () => {
39
+ setAlias("delete-a", "delete-b", "peer");
40
+ setAlias("delete-c", "delete-b", "peer");
41
+ setAlias("delete-b", "delete-c", "other");
42
+ setAlias("delete-c", "delete-d", "keep");
43
+ assert.equal(removeAlias("delete-a", "peer"), true);
44
+ assert.equal(removeAlias("delete-a", "peer"), false);
45
+ assert.equal(aliasTarget("delete-c", "peer"), "delete-b");
46
+ removeSessionAliases("delete-b");
47
+ assert.deepEqual(aliasesFor("delete-b"), []);
48
+ assert.deepEqual(aliasesFor("delete-c"), [{ alias: "keep", sessionId: "delete-d" }]);
49
+ });
50
+
51
+ test("invalid names cannot inject headers or shadow direct Session IDs", () => {
52
+ for (const alias of ["", " ", "x".repeat(101), "x\ny", "x\r", "x\u0000", "x\u0085", "12345678-1234-1234-1234-123456789abc"]) {
53
+ assert.throws(() => validateAlias(alias));
54
+ }
55
+ assert.equal(validateAlias(" 审阅者 "), "审阅者");
56
+ assert.throws(() => setAlias("same", "same", "self"), /itself/);
57
+ });
@@ -0,0 +1,50 @@
1
+ import { db } from "./db.ts";
2
+
3
+ export type SessionAlias = { alias: string; sessionId: string };
4
+
5
+ /** UUID-shaped names are reserved for direct session addressing. */
6
+ export const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+
8
+ export function validateAlias(value: string): string {
9
+ // Check before trimming so trailing line breaks cannot enter a message header.
10
+ if (/[\u0000-\u001f\u007f-\u009f]/u.test(value)) throw new Error("Alias must not contain control characters");
11
+ const alias = value.trim();
12
+ if (!alias || alias.length > 100) throw new Error("Alias must contain between 1 and 100 characters");
13
+ if (SESSION_ID_PATTERN.test(alias)) throw new Error("Alias must not look like a Session ID");
14
+ return alias;
15
+ }
16
+
17
+ export function aliasesFor(owner: string): SessionAlias[] {
18
+ const rows = db.prepare("SELECT alias, target_session_id AS sessionId FROM session_aliases WHERE owner_session_id = ? ORDER BY alias").all(owner) as SessionAlias[];
19
+ return rows.map(({ alias, sessionId }) => ({ alias, sessionId }));
20
+ }
21
+
22
+ export function aliasTarget(owner: string, alias: string): string | undefined {
23
+ return (db.prepare("SELECT target_session_id FROM session_aliases WHERE owner_session_id = ? AND alias = ?").get(owner, alias) as { target_session_id: string } | undefined)?.target_session_id;
24
+ }
25
+
26
+ export function aliasForTarget(owner: string, target: string): string | undefined {
27
+ return (db.prepare("SELECT alias FROM session_aliases WHERE owner_session_id = ? AND target_session_id = ?").get(owner, target) as { alias: string } | undefined)?.alias;
28
+ }
29
+
30
+ /** One nickname per contact. A rename cannot take another contact's nickname. */
31
+ export function setAlias(owner: string, target: string, value: string): void {
32
+ const alias = validateAlias(value);
33
+ if (owner === target) throw new Error("Cannot assign an Alias to this session itself");
34
+ const current = aliasTarget(owner, alias);
35
+ if (current && current !== target) throw new Error("Alias already refers to another session; remove it first");
36
+ // Synchronous SQLite statements have no intervening await; the UNIQUE constraint
37
+ // also protects against another process writing the same database.
38
+ db.prepare(`INSERT INTO session_aliases (owner_session_id, target_session_id, alias) VALUES (?, ?, ?)
39
+ ON CONFLICT(owner_session_id, target_session_id) DO UPDATE SET alias = excluded.alias`).run(owner, target, alias);
40
+ }
41
+
42
+ export function removeAlias(owner: string, value: string): boolean {
43
+ const alias = validateAlias(value);
44
+ return db.prepare("DELETE FROM session_aliases WHERE owner_session_id = ? AND alias = ?").run(owner, alias).changes > 0;
45
+ }
46
+
47
+ /** Called only after actual session deletion, never process stop or idle reap. */
48
+ export function removeSessionAliases(sessionId: string): void {
49
+ db.prepare("DELETE FROM session_aliases WHERE owner_session_id = ? OR target_session_id = ?").run(sessionId, sessionId);
50
+ }
@@ -0,0 +1,71 @@
1
+ /** Native JSONL graph selection shared by presentation and control-state readers.
2
+ * Presentation can stop at the last message; state replay includes metadata at
3
+ * the same branch tail. No message-body normalization belongs in this layer. */
4
+ export type BranchEntry = {
5
+ uuid: string; order: number; type: string; subtype?: string;
6
+ parentUuid?: string | null; logicalParentUuid?: string;
7
+ isSidechain?: boolean; teamName?: string; attachmentType?: string;
8
+ compactMetadata?: { preservedMessages?: { uuids: string[]; anchorUuid: string }; preservedSegment?: { headUuid: string; tailUuid: string; anchorUuid: string } };
9
+ };
10
+
11
+ export class HistoryError extends Error {
12
+ constructor(public status: 400 | 404 | 409, message: string) { super(message); }
13
+ }
14
+
15
+ export const eligibleBranchEntry = (e: BranchEntry, sidechain = false) => (sidechain || !e.isSidechain && !e.teamName)
16
+ && e.type !== "progress" && !(e.type === "attachment" && e.attachmentType === "fork_briefing");
17
+
18
+ export function selectSessionBranch<T extends BranchEntry>(entries: ReadonlyMap<string, T>, anchor?: string | null, sidechain = false, includeTail = false): { chain: T[]; tail?: T; cutoff: number } {
19
+ const cutoff = anchor ? entries.get(anchor)?.order ?? Infinity : Infinity;
20
+ const parents = new Map([...entries].map(([id, e]) => [id, e.parentUuid]));
21
+ for (const e of entries.values()) {
22
+ if (e.type !== "system" || e.subtype !== "compact_boundary" || e.order > cutoff) continue;
23
+ const p = e.compactMetadata?.preservedMessages;
24
+ const s = e.compactMetadata?.preservedSegment;
25
+ if (p?.uuids.length && p.uuids.every((id) => entries.has(id))) {
26
+ let previous = p.anchorUuid;
27
+ for (const id of p.uuids) { parents.set(id, previous); previous = id; }
28
+ for (const [id, parent] of parents) if (parent === p.anchorUuid && id !== p.uuids[0]) parents.set(id, previous);
29
+ } else if (s && entries.has(s.headUuid)) {
30
+ parents.set(s.headUuid, s.anchorUuid);
31
+ for (const [id, parent] of parents) if (parent === s.anchorUuid && id !== s.headUuid) parents.set(id, s.tailUuid);
32
+ }
33
+ }
34
+ const eligible = (e: T) => eligibleBranchEntry(e, sidechain);
35
+ const parentIds = new Set([...entries.values()].filter(eligible).map((e) => parents.get(e.uuid)).filter(Boolean));
36
+ const leaves = [...entries.values()].filter((e) => eligible(e) && !parentIds.has(e.uuid)).sort((a, b) => b.order - a.order);
37
+ let tail: T | undefined;
38
+ for (const leaf of leaves) {
39
+ let e: T | undefined = leaf;
40
+ const visited = new Set<string>();
41
+ while (e && !visited.has(e.uuid)) {
42
+ visited.add(e.uuid);
43
+ if (e.type === "user" || e.type === "assistant") { tail = includeTail ? leaf : e; break; }
44
+ e = entries.get(parents.get(e.uuid) ?? "");
45
+ }
46
+ if (tail) break;
47
+ }
48
+ if (anchor) {
49
+ const found = entries.get(anchor);
50
+ if (!found) throw new HistoryError(409, "回退位置已变化,请重新加载会话");
51
+ tail = found;
52
+ }
53
+ const seen = new Set<string>();
54
+ const chain: T[] = [];
55
+ let e = tail;
56
+ let beforeCompaction = false;
57
+ const walked = new Set<string>();
58
+ while (e) {
59
+ const visit = `${beforeCompaction}:${e.uuid}`;
60
+ if (walked.has(visit)) break;
61
+ walked.add(visit);
62
+ if (!seen.has(e.uuid)) { seen.add(e.uuid); chain.push(e); }
63
+ // Once crossing a boundary, follow original links through preserved duplicates.
64
+ // The current-context rewrites would otherwise lead back into the same summary.
65
+ let parent = beforeCompaction ? e.parentUuid : parents.get(e.uuid);
66
+ if (!parent && e.subtype === "compact_boundary" && e.logicalParentUuid) { parent = e.logicalParentUuid; beforeCompaction = true; }
67
+ e = entries.get(parent ?? "");
68
+ }
69
+ chain.reverse();
70
+ return { chain, tail, cutoff };
71
+ }