@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,73 @@
1
+ import type { Hono } from "hono";
2
+ import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
3
+ import { db } from "./db.ts";
4
+ import { HttpError, projectPath } from "./sessions.ts";
5
+ import { historyCwd } from "./session-dirs.ts";
6
+ import { createBinding, saveSource, sourceInfo, sourceRow } from "./http-channel-store.ts";
7
+ import { httpChannels } from "./http-channel-manager.ts";
8
+ import type { HttpBindingOverrides, HttpChannelDirection, HttpSourceInput } from "../shared/http-channels.ts";
9
+
10
+ /** Registered after the shared authentication and same-origin middleware. */
11
+ export function registerHttpChannelRoutes(app: Hono) {
12
+ app.get("/api/http-channels", (c) => c.json(httpChannels.info(c.req.query("sessionId"))));
13
+ app.get("/api/http-channels/sources/:id", (c) => {
14
+ const source = sourceRow(c.req.param("id"));
15
+ if (!source) throw new HttpError(404, "HTTP 源不存在");
16
+ const headers = new Headers(JSON.parse(source.headers_json));
17
+ headers.delete("content-type");
18
+ c.header("Cache-Control", "no-store");
19
+ return c.json({ ...sourceInfo(source), hasHeaders: [...headers].length > 0, headers: Object.fromEntries(headers), extraBody: JSON.parse(source.extra_body_json) });
20
+ });
21
+
22
+ app.post("/api/http-channels/sources", async (c) => {
23
+ const body = await c.req.json().catch(() => null);
24
+ if (!body || typeof body !== "object") throw new HttpError(400, "请提供源配置");
25
+ return c.json(sourceInfo(saveSource(body as HttpSourceInput)), 201);
26
+ });
27
+ app.put("/api/http-channels/sources/:id", async (c) => {
28
+ const id = c.req.param("id");
29
+ if (!sourceRow(id)) throw new HttpError(404, "HTTP 源不存在");
30
+ const body = await c.req.json().catch(() => null);
31
+ if (!body || typeof body !== "object") throw new HttpError(400, "请提供源配置");
32
+ const source = saveSource(body as HttpSourceInput, id);
33
+ httpChannels.sourceChanged(id);
34
+ return c.json(sourceInfo(source));
35
+ });
36
+ app.delete("/api/http-channels/sources/:id", (c) => {
37
+ httpChannels.removeSource(c.req.param("id"));
38
+ return c.json({ ok: true });
39
+ });
40
+
41
+ app.post("/api/projects/:pid/sessions/:sid/http-bindings", async (c) => {
42
+ const project = projectPath(c.req.param("pid"));
43
+ const sessionId = c.req.param("sid");
44
+ const body = await c.req.json().catch(() => null);
45
+ if (!body || typeof body.sourceId !== "string" || !["subscribe", "push"].includes(body.direction)) throw new HttpError(400, "请选择 HTTP 源和方向");
46
+ // CLI-created sessions also need a durable owner record for background restoration.
47
+ if (!db.prepare("SELECT 1 FROM session_view WHERE session_id=?").get(sessionId)) {
48
+ if (!await getSessionInfo(sessionId, { dir: await historyCwd(project, sessionId) }).catch(() => undefined)) throw new HttpError(404, "会话不存在");
49
+ db.prepare("INSERT OR IGNORE INTO session_view (session_id, project_path) VALUES (?, ?)").run(sessionId, project);
50
+ }
51
+ const binding = createBinding(sessionId, body.sourceId, body.direction as HttpChannelDirection, body as HttpBindingOverrides);
52
+ return c.json({ id: binding.id }, 201);
53
+ });
54
+
55
+ app.patch("/api/http-channels/bindings/:id", async (c) => {
56
+ const body = await c.req.json().catch(() => null);
57
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new HttpError(400, "请提供注册配置");
58
+ httpChannels.updateBinding(c.req.param("id"), body as HttpBindingOverrides);
59
+ return c.json({ ok: true });
60
+ });
61
+
62
+ app.post("/api/http-channels/bindings/:id/activation", async (c) => {
63
+ const body = await c.req.json().catch(() => null);
64
+ if (typeof body?.active !== "boolean") throw new HttpError(400, "请指定激活状态");
65
+ const id = c.req.param("id");
66
+ const deactivatedId = body.active ? httpChannels.activate(id) : (httpChannels.deactivate(id), null);
67
+ return c.json({ ok: true, deactivatedId });
68
+ });
69
+ app.delete("/api/http-channels/bindings/:id", (c) => {
70
+ httpChannels.removeBinding(c.req.param("id"));
71
+ return c.json({ ok: true });
72
+ });
73
+ }
@@ -0,0 +1,142 @@
1
+ import crypto from "node:crypto";
2
+ import { validateHeaderValue } from "node:http";
3
+ import { db } from "./db.ts";
4
+ import { HttpError } from "./sessions.ts";
5
+ import { encodeProjectId } from "../shared/protocol.ts";
6
+ import type { HttpBindingOverrides, HttpChannelBinding, HttpChannelDirection, HttpSource, HttpSourceInput, HttpSubscribeMethod, HttpSourceKind } from "../shared/http-channels.ts";
7
+
8
+ export type SourceRow = { id: string; name: string; url: string; headers_json: string; timeout_ms: number; extra_body_json: string; subscribe_method: HttpSubscribeMethod; kind: HttpSourceKind; brain_id: string | null; brain_voice: string | null };
9
+ export type BindingRow = { id: string; session_id: string; source_id: string; direction: HttpChannelDirection; brain_id: string | null; brain_voice: string | null; active: number; activated_at: number | null; inactive_reason: string | null };
10
+
11
+ export function effectiveBrain(source: SourceRow, binding: BindingRow) {
12
+ return { brainId: binding.brain_id ?? source.brain_id, brainVoice: binding.brain_voice ?? source.brain_voice };
13
+ }
14
+
15
+ function validatedBrainId(value: unknown): string | null {
16
+ if (value === undefined || value === null) return null;
17
+ if (typeof value !== "string" || value.length > 256) throw new HttpError(400, "brain_id 应为字符串(最多 256 字)");
18
+ return value.trim() || null;
19
+ }
20
+
21
+ function validatedBrainVoice(value: unknown): string | null {
22
+ if (value === undefined || value === null) return null;
23
+ if (typeof value !== "string" || value.length > 1024) throw new HttpError(400, "x-brain-voice 应为字符串(最多 1024 字)");
24
+ try {
25
+ validateHeaderValue("x-brain-voice", value);
26
+ return new Headers({ "x-brain-voice": value }).get("x-brain-voice")?.trim() ?? "";
27
+ } catch { throw new HttpError(400, "x-brain-voice 请求头值无效"); }
28
+ }
29
+
30
+ export function validatedBindingOverrides(source: SourceRow, input: HttpBindingOverrides) {
31
+ if (source.kind !== "brain-api") {
32
+ if (input.brainId != null || input.brainVoice != null) throw new HttpError(400, "只有 brain-api 注册支持 brain 配置");
33
+ return { brainId: null, brainVoice: null };
34
+ }
35
+ return { brainId: validatedBrainId(input.brainId), brainVoice: validatedBrainVoice(input.brainVoice) };
36
+ }
37
+
38
+ export function saveBindingOverrides(id: string, input: HttpBindingOverrides): BindingRow {
39
+ const row = bindingRow(id);
40
+ if (!row) throw new HttpError(404, "注册不存在");
41
+ const source = sourceRow(row.source_id);
42
+ if (!source) throw new HttpError(404, "HTTP 源不存在");
43
+ const data = validatedBindingOverrides(source, {
44
+ brainId: input.brainId === undefined ? row.brain_id : input.brainId,
45
+ brainVoice: input.brainVoice === undefined ? row.brain_voice : input.brainVoice,
46
+ });
47
+ if (row.active && source.kind === "brain-api" && !(data.brainId ?? source.brain_id)) throw new HttpError(400, "激活 brain-api 前,请在源或会话注册中填写 brain_id");
48
+ db.prepare("UPDATE session_http_bindings SET brain_id=?, brain_voice=? WHERE id=?").run(data.brainId, data.brainVoice, id);
49
+ return bindingRow(id)!;
50
+ }
51
+
52
+ export function sourceRow(id: string) {
53
+ return db.prepare("SELECT * FROM http_sources WHERE id = ?").get(id) as SourceRow | undefined;
54
+ }
55
+ export function bindingRow(id: string) {
56
+ return db.prepare("SELECT * FROM session_http_bindings WHERE id = ?").get(id) as BindingRow | undefined;
57
+ }
58
+ export function activeRows() {
59
+ return db.prepare("SELECT * FROM session_http_bindings WHERE active = 1 ORDER BY activated_at DESC, id LIMIT 32").all() as BindingRow[];
60
+ }
61
+ export function sourceInfo(row: SourceRow): HttpSource {
62
+ return { id: row.id, kind: row.kind, brainId: row.brain_id, brainVoice: row.brain_voice, name: row.name, url: row.url, timeoutMs: row.timeout_ms, subscribeMethod: row.subscribe_method, hasHeaders: row.headers_json !== "{}", hasExtraBody: row.extra_body_json !== "{}" };
63
+ }
64
+ export function listSources() {
65
+ return (db.prepare("SELECT * FROM http_sources ORDER BY name, id").all() as SourceRow[]).map(sourceInfo);
66
+ }
67
+ export function listBindings(sessionId?: string): HttpChannelBinding[] {
68
+ const query = `SELECT b.*, v.project_path FROM session_http_bindings b JOIN session_view v ON v.session_id = b.session_id
69
+ ${sessionId ? "WHERE b.session_id = ?" : ""} ORDER BY b.active DESC, b.created_at DESC`;
70
+ const statement = db.prepare(query);
71
+ const rows = (sessionId ? statement.all(sessionId) : statement.all()) as (BindingRow & { project_path: string })[];
72
+ return rows.map((row) => ({
73
+ id: row.id, sessionId: row.session_id, sourceId: row.source_id, projectId: encodeProjectId(row.project_path),
74
+ direction: row.direction, brainId: row.brain_id, brainVoice: row.brain_voice, active: !!row.active, activatedAt: row.activated_at, inactiveReason: row.inactive_reason, stats: null,
75
+ }));
76
+ }
77
+
78
+ function validatedSource(input: HttpSourceInput) {
79
+ const kind = input.kind === undefined ? "http" : input.kind;
80
+ if (kind !== "http" && kind !== "brain-api") throw new HttpError(400, "源类型无效");
81
+ let brainId: string | null = null;
82
+ let brainVoice: string | null = null;
83
+ if (kind === "brain-api") {
84
+ brainId = validatedBrainId(input.brainId);
85
+ brainVoice = validatedBrainVoice(input.brainVoice) || null;
86
+ }
87
+ const name = kind === "brain-api" && (input.name === undefined || input.name === "") ? brainId ? `brain-api · ${brainId.slice(0, 80)}` : "brain-api" : input.name;
88
+ if (typeof name !== "string" || !name.trim() || name.length > 100) throw new HttpError(400, "请填写源名称(最多 100 字)");
89
+ if (typeof input.url !== "string" || input.url.length > 4096) throw new HttpError(400, "请填写 HTTP 地址");
90
+ let url: URL;
91
+ try { url = new URL(input.url); } catch { throw new HttpError(400, "HTTP 地址无效"); }
92
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash) throw new HttpError(400, "请使用 HTTP/HTTPS 地址;鉴权信息请放在请求头中");
93
+ const timeout = input.timeoutMs ?? 120_000;
94
+ if (!Number.isInteger(timeout) || timeout < 1000 || timeout > 600_000) throw new HttpError(400, "请求超时应为 1~600 秒");
95
+ const subscribeMethod = kind === "brain-api" ? "POST" : input.subscribeMethod === undefined ? "GET" : input.subscribeMethod;
96
+ if (subscribeMethod !== "GET" && subscribeMethod !== "POST") throw new HttpError(400, "订阅请求方式只能是 GET 或 POST");
97
+ const headers = input.headers ?? {};
98
+ if (!headers || typeof headers !== "object" || Array.isArray(headers) || Object.values(headers).some((value) => typeof value !== "string") || JSON.stringify(headers).length > 16_384) throw new HttpError(400, "请求头应为 JSON 字符串键值对象,最大 16KB");
99
+ let normalized: Headers;
100
+ try { normalized = new Headers(headers); } catch { throw new HttpError(400, "请求头无效"); }
101
+ // Content-Type is owned by the JSON protocol, never by source configuration.
102
+ normalized.delete("content-type");
103
+ const extraBody = kind === "brain-api" || input.extraBody === undefined ? {} : input.extraBody;
104
+ if (!extraBody || typeof extraBody !== "object" || Array.isArray(extraBody) || Buffer.byteLength(JSON.stringify(extraBody)) > 16_384) throw new HttpError(400, "Extra body 应为 JSON 对象,最大 16KB");
105
+ const pending = [{ value: extraBody as unknown, depth: 0 }];
106
+ while (pending.length) {
107
+ const { value, depth } = pending.pop()!;
108
+ if (value && typeof value === "object") {
109
+ if (depth > 32) throw new HttpError(400, "Extra body 嵌套最多 32 层");
110
+ for (const child of Object.values(value)) pending.push({ value: child, depth: depth + 1 });
111
+ }
112
+ }
113
+ if (["kind", "id", "text", "sessionId"].some((key) => Object.hasOwn(extraBody, key))) throw new HttpError(400, "Extra body 不能覆盖 kind、id、text、sessionId");
114
+ return { kind, brainId, brainVoice, name: name.trim(), url: url.href, headers: JSON.stringify(Object.fromEntries(normalized)), extraBody: JSON.stringify(extraBody), timeout, subscribeMethod };
115
+ }
116
+ export function saveSource(input: HttpSourceInput, id: string = crypto.randomUUID()): SourceRow {
117
+ const previous = sourceRow(id);
118
+ if (previous && input.kind !== undefined && input.kind !== previous.kind) throw new HttpError(400, "已有源不能更改类型,请新建一个源");
119
+ const data = validatedSource({ ...input, kind: input.kind === undefined ? previous?.kind : input.kind, brainId: input.brainId === undefined ? previous?.brain_id : input.brainId, brainVoice: input.brainVoice === undefined ? previous?.brain_voice : input.brainVoice, subscribeMethod: input.subscribeMethod === undefined ? previous?.subscribe_method : input.subscribeMethod, headers: input.headers ?? (previous ? JSON.parse(previous.headers_json) : {}), extraBody: input.extraBody === undefined ? (previous ? JSON.parse(previous.extra_body_json) : {}) : input.extraBody });
120
+ db.prepare(`INSERT INTO http_sources (id, name, url, headers_json, timeout_ms, extra_body_json, subscribe_method, kind, brain_id, brain_voice) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
121
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name, url=excluded.url, headers_json=excluded.headers_json, timeout_ms=excluded.timeout_ms, extra_body_json=excluded.extra_body_json, subscribe_method=excluded.subscribe_method, kind=excluded.kind, brain_id=excluded.brain_id, brain_voice=excluded.brain_voice`)
122
+ .run(id, data.name, data.url, data.headers, data.timeout, data.extraBody, data.subscribeMethod, data.kind, data.brainId, data.brainVoice);
123
+ return sourceRow(id)!;
124
+ }
125
+ export function createBinding(sessionId: string, sourceId: string, direction: HttpChannelDirection, overrides: HttpBindingOverrides = {}): BindingRow {
126
+ const source = sourceRow(sourceId);
127
+ if (!source) throw new HttpError(404, "HTTP 源不存在");
128
+ if (!["subscribe", "push"].includes(direction)) throw new HttpError(400, "请选择订阅或推送");
129
+ if (source.kind === "brain-api" && direction !== "subscribe") throw new HttpError(400, "brain-api 订阅已包含推送,不需要单独注册推送");
130
+ const data = validatedBindingOverrides(source, overrides);
131
+ if (db.prepare("SELECT id FROM session_http_bindings WHERE session_id=? AND source_id=? AND direction=?").get(sessionId, sourceId, direction)) throw new HttpError(409, "这个会话已经注册了该源和方向");
132
+ const id = crypto.randomUUID();
133
+ db.prepare("INSERT INTO session_http_bindings (id, session_id, source_id, direction, brain_id, brain_voice, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
134
+ .run(id, sessionId, sourceId, direction, data.brainId, data.brainVoice, Date.now());
135
+ return bindingRow(id)!;
136
+ }
137
+ export function setBindingActive(id: string, active: boolean, reason: string | null = null) {
138
+ // Monotonic activation time gives FIFO eviction a deterministic order even within one millisecond.
139
+ const max = (db.prepare("SELECT MAX(activated_at) AS latest FROM session_http_bindings").get() as { latest: number | null }).latest ?? 0;
140
+ db.prepare("UPDATE session_http_bindings SET active=?, activated_at=CASE WHEN ?=1 THEN ? ELSE activated_at END, inactive_reason=? WHERE id=?")
141
+ .run(active ? 1 : 0, active ? 1 : 0, Math.max(Date.now(), max + 1), reason, id);
142
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
6
+ import { Hono } from "hono";
7
+
8
+ type Message = Record<string, any>;
9
+ class FakeQuery {
10
+ received: Message[] = [];
11
+ private messages: Message[] = [];
12
+ private waiter?: (message: IteratorResult<Message>) => void;
13
+ private closed = false;
14
+ constructor(readonly args: { prompt: AsyncIterable<Message>; options: Record<string, any> }) {
15
+ void (async () => { for await (const message of args.prompt) this.received.push(message); })();
16
+ }
17
+ push(message: Message) {
18
+ if (this.waiter) { const waiter = this.waiter; this.waiter = undefined; waiter({ value: message, done: false }); }
19
+ else this.messages.push(message);
20
+ }
21
+ close() { this.closed = true; this.waiter?.({ value: undefined, done: true }); }
22
+ async interrupt() {}
23
+ async applyFlagSettings() {}
24
+ [Symbol.asyncIterator]() {
25
+ return { next: (): Promise<IteratorResult<Message>> => {
26
+ const message = this.messages.shift();
27
+ if (message) return Promise.resolve({ value: message, done: false });
28
+ if (this.closed) return Promise.resolve({ value: undefined, done: true });
29
+ return new Promise((resolve) => { this.waiter = resolve; });
30
+ } };
31
+ }
32
+ }
33
+
34
+ /** Each test file gets a private DB, private CLI directory and no real SDK subprocesses. */
35
+ export async function channelFixture() {
36
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cide-http-channels-"));
37
+ process.env.CLAUDE_IDE_DATA_DIR = path.join(tmp, "data");
38
+ process.env.CLAUDE_IDE_PASSWORD = "http-channel-tests";
39
+ process.env.CLAUDE_CONFIG_DIR = path.join(tmp, "claude");
40
+ const live = await import("./live-session.ts");
41
+ const queries: FakeQuery[] = [];
42
+ live.setQueryImplForTests(((args: FakeQuery["args"]) => { const query = new FakeQuery(args); queries.push(query); return query; }) as never);
43
+ const { sessions, addProject, createDraftSession } = await import("./sessions.ts");
44
+ const { db } = await import("./db.ts");
45
+ const store = await import("./http-channel-store.ts");
46
+ const { httpChannels, HttpChannelManager } = await import("./http-channel-manager.ts");
47
+ const { registerHttpChannelRoutes } = await import("./http-channel-routes.ts");
48
+ const project = await addProject(tmp);
49
+ const app = new Hono();
50
+ app.onError((error, context) => context.json({ error: error.message }, "status" in error ? error.status as 400 : 500));
51
+ registerHttpChannelRoutes(app);
52
+ const newSession = () => createDraftSession(tmp);
53
+ const queryFor = (id: string) => queries.find((query) => (query.args.options.sessionId ?? query.args.options.resume) === id);
54
+ const servers: { close: () => Promise<void> }[] = [];
55
+ const managers = new Set([httpChannels]);
56
+ return {
57
+ tmp, project, app, queries, queryFor, newSession, sessions, db, store, httpChannels,
58
+ fastManager() {
59
+ const manager = new HttpChannelManager(async () => { await new Promise<void>((resolve) => setImmediate(resolve)); });
60
+ managers.add(manager);
61
+ return manager;
62
+ },
63
+ async http(handler: (request: IncomingMessage, response: ServerResponse) => void | Promise<void>) {
64
+ const server = createServer((request, response) => { Promise.resolve(handler(request, response)).catch(() => { response.statusCode = 500; response.end(); }); });
65
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
66
+ const address = server.address();
67
+ if (!address || typeof address === "string") throw new Error("missing test server address");
68
+ const close = async () => {
69
+ server.closeAllConnections();
70
+ await new Promise<void>((resolve) => server.close(() => resolve()));
71
+ };
72
+ servers.push({ close });
73
+ return { url: `http://127.0.0.1:${address.port}`, close };
74
+ },
75
+ async reset() {
76
+ for (const manager of managers) manager.stop();
77
+ sessions.closeAll();
78
+ await new Promise<void>((resolve) => setImmediate(resolve));
79
+ db.exec("DELETE FROM session_http_bindings; DELETE FROM http_sources;");
80
+ queries.length = 0;
81
+ },
82
+ async close() {
83
+ for (const manager of managers) manager.stop();
84
+ sessions.closeAll();
85
+ for (const server of servers) await server.close();
86
+ await new Promise<void>((resolve) => setImmediate(resolve));
87
+ db.close();
88
+ fs.rmSync(tmp, { recursive: true, force: true });
89
+ },
90
+ text(id: string, text: string, messageId = "same-api-message") {
91
+ return { type: "assistant", uuid: id, parent_tool_use_id: null, message: { id: messageId, model: "mock", content: [{ type: "text", text }] } };
92
+ },
93
+ id: () => crypto.randomUUID(),
94
+ };
95
+ }
96
+
97
+ export async function until(condition: () => boolean, timeoutMs = 2500) {
98
+ const deadline = Date.now() + timeoutMs;
99
+ while (!condition()) {
100
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for HTTP channel behavior");
101
+ await new Promise((resolve) => setTimeout(resolve, 5));
102
+ }
103
+ }
@@ -0,0 +1,82 @@
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-messages-tools-"));
8
+ process.env.CLAUDE_IDE_DATA_DIR = dir;
9
+ process.env.CLAUDE_IDE_PASSWORD = "synthetic-messages-test";
10
+ const { db } = await import("./db.ts");
11
+ const { ideMessageTools, ideMessagesServer } = await import("./ide-messages-mcp.ts");
12
+ const { configureSessionMessaging } = await import("./session-messaging.ts");
13
+ const { setAlias, aliasTarget } = await import("./session-aliases.ts");
14
+ after(async () => { db.close(); await fs.rm(dir, { recursive: true, force: true }); });
15
+
16
+ const sender = "11111111-1111-4111-8111-111111111111";
17
+ const target = "22222222-2222-4222-8222-222222222222";
18
+ const other = "33333333-3333-4333-8333-333333333333";
19
+
20
+ test("actual MCP handlers bind caller identity and cover address book, delivery, errors, and permission changes", async () => {
21
+ let writable = true;
22
+ const delivered: { id: string; body: string }[] = [];
23
+ configureSessionMessaging({
24
+ resolveExistingSession: async (id) => [sender, target, other].includes(id) ? { sessionId: id, projectPath: `/projects/${id}`, cwd: `/projects/${id}` } : undefined,
25
+ getOrCreate: async (id) => ({ cwd: `/projects/${id}`, sendPeerText: async (_uuid, body, active) => {
26
+ if (!active()) return false;
27
+ delivered.push({ id, body });
28
+ return true;
29
+ } }),
30
+ assertAllowed: () => { if (!writable) throw new Error("source is read-only"); },
31
+ });
32
+ const tools = ideMessageTools(sender);
33
+ assert.deepEqual(tools.map((tool) => tool.name), ["ide_send_message", "ide_set_alias", "ide_list_alias", "ide_remove_alias"]);
34
+ assert.equal(ideMessagesServer(sender).type, "sdk");
35
+ async function invoke(name: string, args: Record<string, unknown> = {}) {
36
+ const tool = tools.find((tool) => tool.name === name)!;
37
+ // Simulate the SDK's parsed input dispatch, using the real tool handler.
38
+ const response = await (tool.handler as (args: Record<string, unknown>, extra: unknown) => Promise<{ content: { type: string; text?: string }[]; isError?: boolean }>)(args, {});
39
+ const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
40
+ return { ...response, text };
41
+ }
42
+ let response = await invoke("ide_list_alias");
43
+ assert.deepEqual(JSON.parse(response.text), { session_id: sender, aliases: [] });
44
+ response = await invoke("ide_set_alias", { session_id: target, alias: "reviewer", from: other });
45
+ assert.equal(response.isError, undefined);
46
+ assert.equal(aliasTarget(sender, "reviewer"), target);
47
+ assert.equal(aliasTarget(other, "reviewer"), undefined);
48
+ setAlias(target, sender, "planner");
49
+ response = await invoke("ide_send_message", { to: "reviewer", text: "/btw ordinary peer text", from: other });
50
+ assert.deepEqual(JSON.parse(response.text), { sessionId: target, status: "queued" });
51
+ assert.equal(delivered.length, 1);
52
+ assert.equal(delivered[0]!.id, target);
53
+ assert.ok(delivered[0]!.body.includes(`Message from session ${sender}`));
54
+ assert.ok(delivered[0]!.body.includes('Source alias: "planner"'));
55
+ assert.ok(delivered[0]!.body.endsWith("/btw ordinary peer text"));
56
+ assert.ok(!delivered[0]!.body.includes(other));
57
+ response = await invoke("ide_set_alias", { session_id: other, alias: "reviewer" });
58
+ assert.equal(response.isError, true);
59
+ assert.match(response.text, /already refers/);
60
+ response = await invoke("ide_send_message", { to: "missing", text: "no delivery" });
61
+ assert.equal(response.isError, true);
62
+ assert.equal(delivered.length, 1);
63
+
64
+ writable = false;
65
+ for (const [name, args] of [
66
+ ["ide_send_message", { to: "reviewer", text: "blocked" }],
67
+ ["ide_set_alias", { session_id: other, alias: "new" }],
68
+ ["ide_remove_alias", { alias: "reviewer" }],
69
+ ] as const) {
70
+ response = await invoke(name, args);
71
+ assert.equal(response.isError, true);
72
+ assert.match(response.text, /read-only/);
73
+ }
74
+ assert.equal((await invoke("ide_list_alias")).isError, undefined);
75
+ assert.equal(aliasTarget(sender, "reviewer"), target);
76
+ assert.equal(delivered.length, 1);
77
+ writable = true;
78
+ response = await invoke("ide_remove_alias", { alias: "reviewer" });
79
+ assert.deepEqual(JSON.parse(response.text), { removed: true });
80
+ assert.equal(aliasTarget(sender, "reviewer"), undefined);
81
+ assert.equal((await invoke("ide_remove_alias", { alias: "reviewer" })).isError, true);
82
+ });
@@ -0,0 +1,47 @@
1
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
2
+ import { z } from "zod";
3
+ import { aliasesFor } from "./session-aliases.ts";
4
+ import { sessionMessaging } from "./session-messaging.ts";
5
+
6
+ function result(value: unknown, isError = false) {
7
+ return { content: [{ type: "text" as const, text: typeof value === "string" ? value : JSON.stringify(value) }], ...(isError ? { isError: true } : {}) };
8
+ }
9
+
10
+ /** Identity is closed over by the host, never accepted from tool arguments. */
11
+ export function ideMessageTools(sessionId: string) {
12
+ const guarded = (action: () => unknown | Promise<unknown>) => async () => {
13
+ try { return result(await action()); }
14
+ catch (error) { return result(error instanceof Error ? error.message : String(error), true); }
15
+ };
16
+ return [
17
+ tool("ide_send_message", "Send text to an existing independent claude-ide session, addressed by Session ID or an Alias in your own address book. Success means queued, not read or answered. Do not automatically reply to every received message.", {
18
+ to: z.string().trim().min(1).max(100).describe("Target Session ID or your Alias for it"),
19
+ text: z.string().min(1).max(100_000).describe("Message text; treated as text, not slash commands"),
20
+ }, async ({ to, text }) => guarded(() => sessionMessaging.send(sessionId, to, text))()),
21
+ tool("ide_set_alias", "Set or rename your private nickname for an existing session. Each contact has one nickname. A nickname already assigned to another contact is rejected. Other sessions' address books are unaffected.", {
22
+ session_id: z.string().uuid().describe("Existing target Session ID"),
23
+ alias: z.string().min(1).max(100).describe("Your nickname for this session; cannot look like a Session ID"),
24
+ }, async ({ session_id, alias }) => guarded(async () => {
25
+ await sessionMessaging.setAlias(sessionId, session_id, alias);
26
+ return { session_id, alias: alias.trim() };
27
+ })()),
28
+ tool("ide_list_alias", "List your private address book, along with your own Session ID. These names are local to your session, not globally registered identities.", {},
29
+ guarded(() => ({ session_id: sessionId, aliases: aliasesFor(sessionId).map(({ alias, sessionId: target }) => ({ alias, session_id: target })) }))),
30
+ tool("ide_remove_alias", "Remove a nickname from your own address book. Does not stop or delete the target session.", {
31
+ alias: z.string().min(1).max(100).describe("Your nickname to remove"),
32
+ }, async ({ alias }) => guarded(async () => {
33
+ if (!await sessionMessaging.removeAlias(sessionId, alias)) throw new Error("Alias not found in this session's address book");
34
+ return { removed: true };
35
+ })()),
36
+ ];
37
+ }
38
+
39
+ export function ideMessagesServer(sessionId: string) {
40
+ return createSdkMcpServer({
41
+ name: "IDEMessages",
42
+ version: "1.0.0",
43
+ alwaysLoad: true,
44
+ instructions: "IDEMessages sends server-mediated messages to independent claude-ide sessions. Aliases are your private address book. Received source_alias is YOUR nickname for the sender, and is empty when you have not named that sender. Source Session IDs are server supplied. Sending only queues a message; it never waits for a reply. Use this tool for independent sessions, not the native subagent SendMessage tool. Avoid automatic reply loops.",
45
+ tools: ideMessageTools(sessionId),
46
+ });
47
+ }
@@ -0,0 +1,65 @@
1
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
2
+ import { z } from "zod";
3
+ import { idecron } from "./idecron.ts";
4
+
5
+ const mode = z.enum(["steer", "next_turn", "interrupt"]);
6
+
7
+ function text(value: string, isError = false) {
8
+ return { content: [{ type: "text" as const, text: value }], ...(isError ? { isError: true } : {}) };
9
+ }
10
+
11
+ /** In-process MCP server injected into one claude-ide session. */
12
+ export function ideCronServer(sessionId: string, projectPath: string | (() => string)) {
13
+ const project = () => typeof projectPath === "function" ? projectPath() : projectPath;
14
+ return createSdkMcpServer({
15
+ name: "IDECron",
16
+ version: "1.0.0",
17
+ alwaysLoad: true,
18
+ instructions: "IDECron owns server-side schedules for this claude-ide session. It fires even when the CLI scheduler is busy: use mode steer to queue into the current turn, or next_turn to wait for an idle session.",
19
+ tools: [
20
+ tool(
21
+ "schedule_create",
22
+ "Create a server-side IDECron schedule for this session. `when` accepts a 5-field cron, an ISO timestamp, or an interval such as 10m/2h/1d. Use mode=steer to deliver at the next safe point of the current turn, next_turn to wait until the session is idle, or interrupt to stop the current turn first.",
23
+ {
24
+ when: z.string().min(1).describe("Cron expression, ISO timestamp, or interval such as 10m/2h/1d"),
25
+ prompt: z.string().min(1).describe("Prompt to send when the schedule fires"),
26
+ recurring: z.boolean().optional().describe("Whether the schedule repeats; defaults to true for cron/interval and false for ISO timestamps"),
27
+ mode: mode.optional().describe("Delivery while the session is busy; defaults to steer"),
28
+ },
29
+ async ({ when, prompt, recurring, mode: delivery }) => {
30
+ try {
31
+ const task = idecron.create({ sessionId, projectPath: project(), when, prompt, recurring, mode: delivery });
32
+ return text(`Created IDECron task ${task.id}. Next run: ${new Date(task.nextRunAt).toISOString()}${task.recurring ? " (recurring)" : " (one-shot)"}.`);
33
+ } catch (err) {
34
+ return text(`Could not create IDECron task: ${err instanceof Error ? err.message : String(err)}`, true);
35
+ }
36
+ },
37
+ ),
38
+ tool(
39
+ "schedule_list",
40
+ "List this session's server-side IDECron schedules.",
41
+ {},
42
+ async () => {
43
+ const tasks = idecron.list(sessionId);
44
+ if (!tasks.length) return text("No IDECron schedules.");
45
+ return text(tasks.map((t) => JSON.stringify({
46
+ id: t.id,
47
+ schedule: t.schedule,
48
+ recurring: t.recurring,
49
+ mode: t.mode,
50
+ nextRun: new Date(t.nextRunAt).toISOString(),
51
+ prompt: t.prompt,
52
+ })).join("\n"));
53
+ },
54
+ ),
55
+ tool(
56
+ "schedule_cancel",
57
+ "Cancel one of this session's server-side IDECron schedules by id.",
58
+ { id: z.string().min(1).describe("IDECron task id") },
59
+ async ({ id }) => idecron.cancel(id, sessionId)
60
+ ? text(`Cancelled IDECron task ${id}.`)
61
+ : text(`No IDECron task ${id} owned by this session.`, true),
62
+ ),
63
+ ],
64
+ });
65
+ }
@@ -0,0 +1,79 @@
1
+ import { test, before } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+
8
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cide-idecron-"));
9
+ process.env.CLAUDE_IDE_DATA_DIR = path.join(tmp, "data");
10
+ process.env.CLAUDE_IDE_PASSWORD = "unit";
11
+
12
+ let idecron: typeof import("./idecron.ts").idecron;
13
+ let db: typeof import("./db.ts").db;
14
+
15
+ before(async () => {
16
+ ({ idecron } = await import("./idecron.ts"));
17
+ ({ db } = await import("./db.ts"));
18
+ });
19
+
20
+ const session = () => crypto.randomUUID();
21
+
22
+ test("IDECron creates, lists, and cancels a session-owned task", () => {
23
+ const sid = session();
24
+ const task = idecron.create({ sessionId: sid, projectPath: "/tmp", when: "10m", prompt: "ping" });
25
+ assert.equal(task.recurring, true);
26
+ assert.equal(task.mode, "steer");
27
+ assert.ok(task.nextRunAt > Date.now());
28
+ assert.deepEqual(idecron.list(sid).map((t) => t.id), [task.id]);
29
+ const cron = idecron.cronsFor(sid)[0]!;
30
+ assert.equal(cron.server, true);
31
+ assert.equal(cron.schedule, "10m");
32
+ assert.equal(cron.recurring, true);
33
+ assert.equal(idecron.cancel(task.id, session()), false, "another session cannot cancel it");
34
+ assert.equal(idecron.cancel(task.id, sid), true);
35
+ assert.deepEqual(idecron.list(sid), []);
36
+ });
37
+
38
+ test("IDECron delivery is coalesced and reschedules recurring tasks", async () => {
39
+ const sid = session();
40
+ const delivered: string[] = [];
41
+ idecron.setDelivery({ canDeliver: () => true, deliver: (t) => { delivered.push(t.id); return true; } });
42
+ const recurring = idecron.create({ sessionId: sid, projectPath: "/tmp", when: "1m", prompt: "tick" });
43
+ db.prepare("UPDATE idecron_schedules SET next_run_at = ? WHERE id = ?").run(Date.now() - 1_000, recurring.id);
44
+ await idecron.tick();
45
+ assert.deepEqual(delivered, [recurring.id]);
46
+ const after = idecron.list(sid)[0]!;
47
+ assert.equal(after.id, recurring.id);
48
+ assert.ok(after.nextRunAt > Date.now());
49
+
50
+ const once = idecron.create({ sessionId: sid, projectPath: "/tmp", when: new Date(Date.now() + 60_000).toISOString(), prompt: "once" });
51
+ db.prepare("UPDATE idecron_schedules SET next_run_at = ? WHERE id = ?").run(Date.now() - 1_000, once.id);
52
+ await idecron.tick();
53
+ assert.deepEqual(delivered, [recurring.id, once.id]);
54
+ assert.deepEqual(idecron.list(sid).map((t) => t.id), [recurring.id]);
55
+ idecron.cancelSession(sid);
56
+ });
57
+
58
+ test("next_turn waits until the session is deliverable", async () => {
59
+ const sid = session();
60
+ let allowed = false;
61
+ const delivered: string[] = [];
62
+ idecron.setDelivery({ canDeliver: () => allowed, deliver: (t) => { delivered.push(t.id); return true; } });
63
+ const task = idecron.create({ sessionId: sid, projectPath: "/tmp", when: new Date(Date.now() + 60_000).toISOString(), prompt: "later", mode: "next_turn" });
64
+ db.prepare("UPDATE idecron_schedules SET next_run_at = ? WHERE id = ?").run(Date.now() - 1_000, task.id);
65
+ await idecron.tick();
66
+ assert.deepEqual(delivered, []);
67
+ assert.equal(idecron.list(sid).length, 1);
68
+ allowed = true;
69
+ await idecron.tick();
70
+ assert.deepEqual(delivered, [task.id]);
71
+ assert.deepEqual(idecron.list(sid), []);
72
+ });
73
+
74
+ test("IDECron rejects invalid schedules", () => {
75
+ const sid = session();
76
+ assert.throws(() => idecron.create({ sessionId: sid, projectPath: "/tmp", when: "not a schedule", prompt: "x" }), /无法解析/);
77
+ assert.throws(() => idecron.create({ sessionId: sid, projectPath: "/tmp", when: "1s", prompt: "x" }), /至少 1 分钟/);
78
+ assert.throws(() => idecron.create({ sessionId: sid, projectPath: "/tmp", when: new Date(Date.now() + 60_000).toISOString(), prompt: "x", recurring: true }), /一次性/);
79
+ });