relay-ide 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (506) hide show
  1. package/README.md +259 -0
  2. package/dist/bin/claude-remote-cli.js +390 -0
  3. package/dist/bin/relay-ide.js +390 -0
  4. package/dist/frontend/assets/abap-BdImnpbu.js +1 -0
  5. package/dist/frontend/assets/actionscript-3-CoDkCxhg.js +1 -0
  6. package/dist/frontend/assets/ada-bCR0ucgS.js +1 -0
  7. package/dist/frontend/assets/andromeeda-C4gqWexZ.js +1 -0
  8. package/dist/frontend/assets/angular-html-DA-rfuFy.js +1 -0
  9. package/dist/frontend/assets/angular-ts-BrjP3tb8.js +1 -0
  10. package/dist/frontend/assets/apache-Pmp26Uib.js +1 -0
  11. package/dist/frontend/assets/apex-D8_7TLub.js +1 -0
  12. package/dist/frontend/assets/apl-CORt7UWP.js +1 -0
  13. package/dist/frontend/assets/applescript-Co6uUVPk.js +1 -0
  14. package/dist/frontend/assets/ara-BRHolxvo.js +1 -0
  15. package/dist/frontend/assets/asciidoc-Ve4PFQV2.js +1 -0
  16. package/dist/frontend/assets/asm-D_Q5rh1f.js +1 -0
  17. package/dist/frontend/assets/astro-HNnZUWAn.js +1 -0
  18. package/dist/frontend/assets/aurora-x-D-2ljcwZ.js +1 -0
  19. package/dist/frontend/assets/awk-DMzUqQB5.js +1 -0
  20. package/dist/frontend/assets/ayu-dark-DYE7WIF3.js +1 -0
  21. package/dist/frontend/assets/ayu-light-BA47KaF1.js +1 -0
  22. package/dist/frontend/assets/ayu-mirage-32ctXXKs.js +1 -0
  23. package/dist/frontend/assets/ballerina-BFfxhgS-.js +1 -0
  24. package/dist/frontend/assets/bat-BkioyH1T.js +1 -0
  25. package/dist/frontend/assets/beancount-k_qm7-4y.js +1 -0
  26. package/dist/frontend/assets/berry-uYugtg8r.js +1 -0
  27. package/dist/frontend/assets/bibtex-CHM0blh-.js +1 -0
  28. package/dist/frontend/assets/bicep-Bmn6On1c.js +1 -0
  29. package/dist/frontend/assets/bird2-BIv1doCn.js +1 -0
  30. package/dist/frontend/assets/blade-BjGOyj-B.js +1 -0
  31. package/dist/frontend/assets/bsl-BO_Y6i37.js +1 -0
  32. package/dist/frontend/assets/c-BIGW1oBm.js +1 -0
  33. package/dist/frontend/assets/c3-eo99z4R2.js +1 -0
  34. package/dist/frontend/assets/cadence-Bv_4Rxtq.js +1 -0
  35. package/dist/frontend/assets/cairo-KRGpt6FW.js +1 -0
  36. package/dist/frontend/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  37. package/dist/frontend/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  38. package/dist/frontend/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  39. package/dist/frontend/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  40. package/dist/frontend/assets/clarity-D53aC0YG.js +1 -0
  41. package/dist/frontend/assets/clojure-P80f7IUj.js +1 -0
  42. package/dist/frontend/assets/cmake-D1j8_8rp.js +1 -0
  43. package/dist/frontend/assets/cobol-nBiQ_Alo.js +1 -0
  44. package/dist/frontend/assets/codeowners-Bp6g37R7.js +1 -0
  45. package/dist/frontend/assets/codeql-DsOJ9woJ.js +1 -0
  46. package/dist/frontend/assets/coffee-Ch7k5sss.js +1 -0
  47. package/dist/frontend/assets/common-lisp-Cg-RD9OK.js +1 -0
  48. package/dist/frontend/assets/coq-DkFqJrB1.js +1 -0
  49. package/dist/frontend/assets/cpp-CofmeUqb.js +1 -0
  50. package/dist/frontend/assets/crystal-DNxU26gB.js +1 -0
  51. package/dist/frontend/assets/csharp-COcwbKMJ.js +1 -0
  52. package/dist/frontend/assets/css-CLj8gQPS.js +1 -0
  53. package/dist/frontend/assets/csv-fuZLfV_i.js +1 -0
  54. package/dist/frontend/assets/cue-D82EKSYY.js +1 -0
  55. package/dist/frontend/assets/cypher-COkxafJQ.js +1 -0
  56. package/dist/frontend/assets/d-85-TOEBH.js +1 -0
  57. package/dist/frontend/assets/dark-plus-C3mMm8J8.js +1 -0
  58. package/dist/frontend/assets/dart-bE4Kk8sk.js +1 -0
  59. package/dist/frontend/assets/dax-CEL-wOlO.js +1 -0
  60. package/dist/frontend/assets/desktop-BmXAJ9_W.js +1 -0
  61. package/dist/frontend/assets/diff-D97Zzqfu.js +1 -0
  62. package/dist/frontend/assets/docker-BcOcwvcX.js +1 -0
  63. package/dist/frontend/assets/dotenv-Da5cRb03.js +1 -0
  64. package/dist/frontend/assets/dracula-BzJJZx-M.js +1 -0
  65. package/dist/frontend/assets/dracula-soft-BXkSAIEj.js +1 -0
  66. package/dist/frontend/assets/dream-maker-BtqSS_iP.js +1 -0
  67. package/dist/frontend/assets/edge-FbVlp4U3.js +1 -0
  68. package/dist/frontend/assets/elixir-CkH2-t6x.js +1 -0
  69. package/dist/frontend/assets/elm-DbKCFpqz.js +1 -0
  70. package/dist/frontend/assets/emacs-lisp-CXvaQtF9.js +1 -0
  71. package/dist/frontend/assets/erb-BYCe7drp.js +1 -0
  72. package/dist/frontend/assets/erlang-DsQrWhSR.js +1 -0
  73. package/dist/frontend/assets/everforest-dark-BgDCqdQA.js +1 -0
  74. package/dist/frontend/assets/everforest-light-C8M2exoo.js +1 -0
  75. package/dist/frontend/assets/fennel-BYunw83y.js +1 -0
  76. package/dist/frontend/assets/fish-BvzEVeQv.js +1 -0
  77. package/dist/frontend/assets/fluent-C4IJs8-o.js +1 -0
  78. package/dist/frontend/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  79. package/dist/frontend/assets/fortran-free-form-BxgE0vQu.js +1 -0
  80. package/dist/frontend/assets/fsharp-CXgrBDvD.js +1 -0
  81. package/dist/frontend/assets/gdresource-BOOCDP_w.js +1 -0
  82. package/dist/frontend/assets/gdscript-C5YyOfLZ.js +1 -0
  83. package/dist/frontend/assets/gdshader-DkwncUOv.js +1 -0
  84. package/dist/frontend/assets/genie-D0YGMca9.js +1 -0
  85. package/dist/frontend/assets/gherkin-DyxjwDmM.js +1 -0
  86. package/dist/frontend/assets/git-commit-F4YmCXRG.js +1 -0
  87. package/dist/frontend/assets/git-rebase-r7XF79zn.js +1 -0
  88. package/dist/frontend/assets/github-dark-DHJKELXO.js +1 -0
  89. package/dist/frontend/assets/github-dark-default-Cuk6v7N8.js +1 -0
  90. package/dist/frontend/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  91. package/dist/frontend/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  92. package/dist/frontend/assets/github-light-DAi9KRSo.js +1 -0
  93. package/dist/frontend/assets/github-light-default-D7oLnXFd.js +1 -0
  94. package/dist/frontend/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  95. package/dist/frontend/assets/gleam-BspZqrRM.js +1 -0
  96. package/dist/frontend/assets/glimmer-js-ByusRIyA.js +1 -0
  97. package/dist/frontend/assets/glimmer-ts-BfAWNZQY.js +1 -0
  98. package/dist/frontend/assets/glsl-DplSGwfg.js +1 -0
  99. package/dist/frontend/assets/gn-n2N0HUVH.js +1 -0
  100. package/dist/frontend/assets/gnuplot-DdkO51Og.js +1 -0
  101. package/dist/frontend/assets/go-C27-OAKa.js +1 -0
  102. package/dist/frontend/assets/graphql-ChdNCCLP.js +1 -0
  103. package/dist/frontend/assets/groovy-gcz8RCvz.js +1 -0
  104. package/dist/frontend/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  105. package/dist/frontend/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  106. package/dist/frontend/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  107. package/dist/frontend/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  108. package/dist/frontend/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  109. package/dist/frontend/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  110. package/dist/frontend/assets/hack-i7_Ulhet.js +1 -0
  111. package/dist/frontend/assets/haml-D5jkg6IW.js +1 -0
  112. package/dist/frontend/assets/handlebars-BpdQsYii.js +1 -0
  113. package/dist/frontend/assets/haskell-Df6bDoY_.js +1 -0
  114. package/dist/frontend/assets/haxe-CzTSHFRz.js +1 -0
  115. package/dist/frontend/assets/hcl-BWvSN4gD.js +1 -0
  116. package/dist/frontend/assets/hjson-D5-asLiD.js +1 -0
  117. package/dist/frontend/assets/hlsl-D3lLCCz7.js +1 -0
  118. package/dist/frontend/assets/horizon-BUw7H-hv.js +1 -0
  119. package/dist/frontend/assets/horizon-bright-CUuTKBJd.js +1 -0
  120. package/dist/frontend/assets/houston-DnULxvSX.js +1 -0
  121. package/dist/frontend/assets/html-derivative-DlHx6ybY.js +1 -0
  122. package/dist/frontend/assets/html-pp8916En.js +1 -0
  123. package/dist/frontend/assets/http-jrhK8wxY.js +1 -0
  124. package/dist/frontend/assets/hurl-irOxFIW8.js +1 -0
  125. package/dist/frontend/assets/hxml-Bvhsp5Yf.js +1 -0
  126. package/dist/frontend/assets/hy-DFXneXwc.js +1 -0
  127. package/dist/frontend/assets/imba-DGztddWO.js +1 -0
  128. package/dist/frontend/assets/ini-BEwlwnbL.js +1 -0
  129. package/dist/frontend/assets/java-CylS5w8V.js +1 -0
  130. package/dist/frontend/assets/javascript-wDzz0qaB.js +1 -0
  131. package/dist/frontend/assets/jinja-f2NsQr07.js +1 -0
  132. package/dist/frontend/assets/jison-wvAkD_A8.js +1 -0
  133. package/dist/frontend/assets/json-Cp-IABpG.js +1 -0
  134. package/dist/frontend/assets/json5-C9tS-k6U.js +1 -0
  135. package/dist/frontend/assets/jsonc-Des-eS-w.js +1 -0
  136. package/dist/frontend/assets/jsonl-DcaNXYhu.js +1 -0
  137. package/dist/frontend/assets/jsonnet-DFQXde-d.js +1 -0
  138. package/dist/frontend/assets/jssm-C2t-YnRu.js +1 -0
  139. package/dist/frontend/assets/jsx-g9-lgVsj.js +1 -0
  140. package/dist/frontend/assets/julia-CxzCAyBv.js +1 -0
  141. package/dist/frontend/assets/just-VxiPbLrw.js +1 -0
  142. package/dist/frontend/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  143. package/dist/frontend/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  144. package/dist/frontend/assets/kanagawa-wave-DWedfzmr.js +1 -0
  145. package/dist/frontend/assets/kdl-DV7GczEv.js +1 -0
  146. package/dist/frontend/assets/kotlin-BdnUsdx6.js +1 -0
  147. package/dist/frontend/assets/kusto-wEQ09or8.js +1 -0
  148. package/dist/frontend/assets/laserwave-DUszq2jm.js +1 -0
  149. package/dist/frontend/assets/latex-CWtU0Tv5.js +1 -0
  150. package/dist/frontend/assets/lean-BZvkOJ9d.js +1 -0
  151. package/dist/frontend/assets/less-B1dDrJ26.js +1 -0
  152. package/dist/frontend/assets/light-plus-B7mTdjB0.js +1 -0
  153. package/dist/frontend/assets/liquid-C0sCDyMI.js +1 -0
  154. package/dist/frontend/assets/llvm-DjAJT7YJ.js +1 -0
  155. package/dist/frontend/assets/log-2UxHyX5q.js +1 -0
  156. package/dist/frontend/assets/logo-BtOb2qkB.js +1 -0
  157. package/dist/frontend/assets/lua-BaeVxFsk.js +1 -0
  158. package/dist/frontend/assets/luau-C-HG3fhB.js +1 -0
  159. package/dist/frontend/assets/main-CL5_Wlhv.css +32 -0
  160. package/dist/frontend/assets/main-Czet4Z1x.js +371 -0
  161. package/dist/frontend/assets/make-CHLpvVh8.js +1 -0
  162. package/dist/frontend/assets/markdown-Cvjx9yec.js +1 -0
  163. package/dist/frontend/assets/marko-DjSrsDqO.js +1 -0
  164. package/dist/frontend/assets/material-theme-D5KoaKCx.js +1 -0
  165. package/dist/frontend/assets/material-theme-darker-BfHTSMKl.js +1 -0
  166. package/dist/frontend/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  167. package/dist/frontend/assets/material-theme-ocean-CyktbL80.js +1 -0
  168. package/dist/frontend/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  169. package/dist/frontend/assets/matlab-D7o27uSR.js +1 -0
  170. package/dist/frontend/assets/mdc-DTYItulj.js +1 -0
  171. package/dist/frontend/assets/mdx-Cmh6b_Ma.js +1 -0
  172. package/dist/frontend/assets/mermaid-mWjccvbQ.js +1 -0
  173. package/dist/frontend/assets/min-dark-CafNBF8u.js +1 -0
  174. package/dist/frontend/assets/min-light-CTRr51gU.js +1 -0
  175. package/dist/frontend/assets/mipsasm-CKIfxQSi.js +1 -0
  176. package/dist/frontend/assets/mojo-rZm6bMo-.js +1 -0
  177. package/dist/frontend/assets/monokai-D4h5O-jR.js +1 -0
  178. package/dist/frontend/assets/moonbit-_H4v1dQx.js +1 -0
  179. package/dist/frontend/assets/move-IF9eRakj.js +1 -0
  180. package/dist/frontend/assets/narrat-DRg8JJMk.js +1 -0
  181. package/dist/frontend/assets/nextflow-C-mBbutL.js +1 -0
  182. package/dist/frontend/assets/nextflow-groovy-vE_lwT2v.js +1 -0
  183. package/dist/frontend/assets/nginx-BpAMiNFr.js +1 -0
  184. package/dist/frontend/assets/night-owl-C39BiMTA.js +1 -0
  185. package/dist/frontend/assets/night-owl-light-CMTm3GFP.js +1 -0
  186. package/dist/frontend/assets/nim-BIad80T-.js +1 -0
  187. package/dist/frontend/assets/nix-CwoSXNpI.js +1 -0
  188. package/dist/frontend/assets/nord-Ddv68eIx.js +1 -0
  189. package/dist/frontend/assets/nushell-Cz2AlsmD.js +1 -0
  190. package/dist/frontend/assets/objective-c-DXmwc3jG.js +1 -0
  191. package/dist/frontend/assets/objective-cpp-CLxacb5B.js +1 -0
  192. package/dist/frontend/assets/ocaml-C0hk2d4L.js +1 -0
  193. package/dist/frontend/assets/odin-BBf5iR-q.js +1 -0
  194. package/dist/frontend/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  195. package/dist/frontend/assets/one-light-C3Wv6jpd.js +1 -0
  196. package/dist/frontend/assets/openscad-C4EeE6gA.js +1 -0
  197. package/dist/frontend/assets/pascal-D93ZcfNL.js +1 -0
  198. package/dist/frontend/assets/perl-NvoQZIq0.js +1 -0
  199. package/dist/frontend/assets/php-R6g_5hLQ.js +1 -0
  200. package/dist/frontend/assets/pkl-u5AG7uiY.js +1 -0
  201. package/dist/frontend/assets/plastic-3e1v2bzS.js +1 -0
  202. package/dist/frontend/assets/plsql-ChMvpjG-.js +1 -0
  203. package/dist/frontend/assets/po-BTJTHyun.js +1 -0
  204. package/dist/frontend/assets/poimandres-CS3Unz2-.js +1 -0
  205. package/dist/frontend/assets/polar-C0HS_06l.js +1 -0
  206. package/dist/frontend/assets/postcss-CXtECtnM.js +1 -0
  207. package/dist/frontend/assets/powerquery-CEu0bR-o.js +1 -0
  208. package/dist/frontend/assets/powershell-Dpen1YoG.js +1 -0
  209. package/dist/frontend/assets/prisma-Dd19v3D-.js +1 -0
  210. package/dist/frontend/assets/prolog-CbFg5uaA.js +1 -0
  211. package/dist/frontend/assets/proto-C7zT0LnQ.js +1 -0
  212. package/dist/frontend/assets/pug-DKIMFp6K.js +1 -0
  213. package/dist/frontend/assets/puppet-BMWR74SV.js +1 -0
  214. package/dist/frontend/assets/purescript-CklMAg4u.js +1 -0
  215. package/dist/frontend/assets/python-B6aJPvgy.js +1 -0
  216. package/dist/frontend/assets/qml-3beO22l8.js +1 -0
  217. package/dist/frontend/assets/qmldir-C8lEn-DE.js +1 -0
  218. package/dist/frontend/assets/qss-IeuSbFQv.js +1 -0
  219. package/dist/frontend/assets/r-Dspwwk_N.js +1 -0
  220. package/dist/frontend/assets/racket-BqYA7rlc.js +1 -0
  221. package/dist/frontend/assets/raku-DXvB9xmW.js +1 -0
  222. package/dist/frontend/assets/razor-BDqjjVU7.js +1 -0
  223. package/dist/frontend/assets/red-bN70gL4F.js +1 -0
  224. package/dist/frontend/assets/reg-C-SQnVFl.js +1 -0
  225. package/dist/frontend/assets/regexp-CDVJQ6XC.js +1 -0
  226. package/dist/frontend/assets/rel-C3B-1QV4.js +1 -0
  227. package/dist/frontend/assets/riscv-BM1_JUlF.js +1 -0
  228. package/dist/frontend/assets/ron-D8l8udqQ.js +1 -0
  229. package/dist/frontend/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  230. package/dist/frontend/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  231. package/dist/frontend/assets/rose-pine-qdsjHGoJ.js +1 -0
  232. package/dist/frontend/assets/rosmsg-BJDFO7_C.js +1 -0
  233. package/dist/frontend/assets/rst-CRjBmOyv.js +1 -0
  234. package/dist/frontend/assets/ruby-Wjq7vjNf.js +1 -0
  235. package/dist/frontend/assets/rust-B1yitclQ.js +1 -0
  236. package/dist/frontend/assets/sas-cz2c8ADy.js +1 -0
  237. package/dist/frontend/assets/sass-Cj5Yp3dK.js +1 -0
  238. package/dist/frontend/assets/scala-C151Ov-r.js +1 -0
  239. package/dist/frontend/assets/scheme-C98Dy4si.js +1 -0
  240. package/dist/frontend/assets/scss-D5BDwBP9.js +1 -0
  241. package/dist/frontend/assets/sdbl-DVxCFoDh.js +1 -0
  242. package/dist/frontend/assets/shaderlab-Dg9Lc6iA.js +1 -0
  243. package/dist/frontend/assets/shellscript-Yzrsuije.js +1 -0
  244. package/dist/frontend/assets/shellsession-BADoaaVG.js +1 -0
  245. package/dist/frontend/assets/slack-dark-BthQWCQV.js +1 -0
  246. package/dist/frontend/assets/slack-ochin-DqwNpetd.js +1 -0
  247. package/dist/frontend/assets/smalltalk-BERRCDM3.js +1 -0
  248. package/dist/frontend/assets/snazzy-light-Bw305WKR.js +1 -0
  249. package/dist/frontend/assets/solarized-dark-DXbdFlpD.js +1 -0
  250. package/dist/frontend/assets/solarized-light-L9t79GZl.js +1 -0
  251. package/dist/frontend/assets/solidity-rGO070M0.js +1 -0
  252. package/dist/frontend/assets/soy-8wufbnw4.js +1 -0
  253. package/dist/frontend/assets/sparql-rVzFXLq3.js +1 -0
  254. package/dist/frontend/assets/splunk-BtCnVYZw.js +1 -0
  255. package/dist/frontend/assets/sql-BLtJtn59.js +1 -0
  256. package/dist/frontend/assets/ssh-config-_ykCGR6B.js +1 -0
  257. package/dist/frontend/assets/stata-BH5u7GGu.js +1 -0
  258. package/dist/frontend/assets/stylus-BEDo0Tqx.js +1 -0
  259. package/dist/frontend/assets/surrealql-Bq5Q-fJD.js +1 -0
  260. package/dist/frontend/assets/svelte-Cy7k_4gC.js +1 -0
  261. package/dist/frontend/assets/swift-D82vCrfD.js +1 -0
  262. package/dist/frontend/assets/synthwave-84-CbfX1IO0.js +1 -0
  263. package/dist/frontend/assets/system-verilog-CnnmHF94.js +1 -0
  264. package/dist/frontend/assets/systemd-4A_iFExJ.js +1 -0
  265. package/dist/frontend/assets/talonscript-CkByrt1z.js +1 -0
  266. package/dist/frontend/assets/tasl-QIJgUcNo.js +1 -0
  267. package/dist/frontend/assets/tcl-dwOrl1Do.js +1 -0
  268. package/dist/frontend/assets/templ-DhtptRzy.js +1 -0
  269. package/dist/frontend/assets/terraform-BETggiCN.js +1 -0
  270. package/dist/frontend/assets/tex-idrVyKtj.js +1 -0
  271. package/dist/frontend/assets/tokyo-night-hegEt444.js +1 -0
  272. package/dist/frontend/assets/toml-vGWfd6FD.js +1 -0
  273. package/dist/frontend/assets/ts-tags-DQrlYJgV.js +1 -0
  274. package/dist/frontend/assets/tsv-B_m7g4N7.js +1 -0
  275. package/dist/frontend/assets/tsx-COt5Ahok.js +1 -0
  276. package/dist/frontend/assets/turtle-BsS91CYL.js +1 -0
  277. package/dist/frontend/assets/twig-xg9kU7Mw.js +1 -0
  278. package/dist/frontend/assets/typescript-BPQ3VLAy.js +1 -0
  279. package/dist/frontend/assets/typespec-CAFt9gP4.js +1 -0
  280. package/dist/frontend/assets/typst-DHCkPAjA.js +1 -0
  281. package/dist/frontend/assets/v-BcVCzyr7.js +1 -0
  282. package/dist/frontend/assets/vala-CsfeWuGM.js +1 -0
  283. package/dist/frontend/assets/vb-D17OF-Vu.js +1 -0
  284. package/dist/frontend/assets/verilog-BQ8w6xss.js +1 -0
  285. package/dist/frontend/assets/vesper-DU1UobuO.js +1 -0
  286. package/dist/frontend/assets/vhdl-CeAyd5Ju.js +1 -0
  287. package/dist/frontend/assets/viml-CJc9bBzg.js +1 -0
  288. package/dist/frontend/assets/vitesse-black-Bkuqu6BP.js +1 -0
  289. package/dist/frontend/assets/vitesse-dark-D0r3Knsf.js +1 -0
  290. package/dist/frontend/assets/vitesse-light-CVO1_9PV.js +1 -0
  291. package/dist/frontend/assets/vue-D2xRrEX4.js +1 -0
  292. package/dist/frontend/assets/vue-html-AaS7Mt5G.js +1 -0
  293. package/dist/frontend/assets/vue-vine-BoDAl6tE.js +1 -0
  294. package/dist/frontend/assets/vyper-CDx5xZoG.js +1 -0
  295. package/dist/frontend/assets/wasm-CG6Dc4jp.js +1 -0
  296. package/dist/frontend/assets/wasm-MzD3tlZU.js +1 -0
  297. package/dist/frontend/assets/wenyan-BV7otONQ.js +1 -0
  298. package/dist/frontend/assets/wgsl-Dx-B1_4e.js +1 -0
  299. package/dist/frontend/assets/wikitext-BhOHFoWU.js +1 -0
  300. package/dist/frontend/assets/wit-5i3qLPDT.js +1 -0
  301. package/dist/frontend/assets/wolfram-lXgVvXCa.js +1 -0
  302. package/dist/frontend/assets/xml-sdJ4AIDG.js +1 -0
  303. package/dist/frontend/assets/xsl-CtQFsRM5.js +1 -0
  304. package/dist/frontend/assets/yaml-Buea-lGh.js +1 -0
  305. package/dist/frontend/assets/zenscript-DVFEvuxE.js +1 -0
  306. package/dist/frontend/assets/zig-VOosw3JB.js +1 -0
  307. package/dist/frontend/icon-192.png +0 -0
  308. package/dist/frontend/icon-512.png +0 -0
  309. package/dist/frontend/icon.svg +8 -0
  310. package/dist/frontend/index.html +30 -0
  311. package/dist/frontend/manifest.json +25 -0
  312. package/dist/frontend/sw.js +66 -0
  313. package/dist/server/agent-events.js +39 -0
  314. package/dist/server/analytics.js +885 -0
  315. package/dist/server/auth.js +65 -0
  316. package/dist/server/belayer/executor.js +200 -0
  317. package/dist/server/belayer/intake.js +27 -0
  318. package/dist/server/belayer/pipeline.js +97 -0
  319. package/dist/server/belayer/pr-lifecycle.js +69 -0
  320. package/dist/server/belayer/prompts.js +154 -0
  321. package/dist/server/belayer/types.js +23 -0
  322. package/dist/server/branch-linker.js +137 -0
  323. package/dist/server/browser-content.js +145 -0
  324. package/dist/server/clipboard.js +63 -0
  325. package/dist/server/codex-hooks-adapter.js +93 -0
  326. package/dist/server/config.js +325 -0
  327. package/dist/server/gh-routes.js +163 -0
  328. package/dist/server/gh.js +276 -0
  329. package/dist/server/git-routes.js +154 -0
  330. package/dist/server/git.js +694 -0
  331. package/dist/server/github-app.js +218 -0
  332. package/dist/server/github-graphql.js +178 -0
  333. package/dist/server/hooks.js +373 -0
  334. package/dist/server/index.js +1549 -0
  335. package/dist/server/integration-github.js +137 -0
  336. package/dist/server/integration-jira.js +210 -0
  337. package/dist/server/integration-linear.js +176 -0
  338. package/dist/server/logger.js +18 -0
  339. package/dist/server/mobile-input-pipeline.js +129 -0
  340. package/dist/server/opencode-relay.js +53 -0
  341. package/dist/server/org-dashboard.js +241 -0
  342. package/dist/server/output-parsers/claude-parser.js +56 -0
  343. package/dist/server/output-parsers/codex-parser.js +13 -0
  344. package/dist/server/output-parsers/index.js +14 -0
  345. package/dist/server/output-parsers/null-parser.js +12 -0
  346. package/dist/server/output-parsers/opencode-parser.js +77 -0
  347. package/dist/server/pty-handler.js +586 -0
  348. package/dist/server/push.js +84 -0
  349. package/dist/server/review-poller.js +237 -0
  350. package/dist/server/sdk-handler.js +539 -0
  351. package/dist/server/service.js +189 -0
  352. package/dist/server/sessions.js +638 -0
  353. package/dist/server/telemetry.js +236 -0
  354. package/dist/server/ticket-transitions.js +166 -0
  355. package/dist/server/types.js +146 -0
  356. package/dist/server/utils.js +23 -0
  357. package/dist/server/watcher.js +661 -0
  358. package/dist/server/webhook-manager.js +547 -0
  359. package/dist/server/webhooks.js +73 -0
  360. package/dist/server/workspace-groups.js +363 -0
  361. package/dist/server/workspaces.js +1207 -0
  362. package/dist/server/ws.js +192 -0
  363. package/dist/test/EmptyState.spec.js +51 -0
  364. package/dist/test/action-coverage.test.js +139 -0
  365. package/dist/test/actions/registry.test.js +59 -0
  366. package/dist/test/actions/shortcuts.test.js +79 -0
  367. package/dist/test/agent-events.test.js +151 -0
  368. package/dist/test/analytics.test.js +158 -0
  369. package/dist/test/attention.test.js +91 -0
  370. package/dist/test/auth.test.js +105 -0
  371. package/dist/test/backend-state.test.js +47 -0
  372. package/dist/test/belayer-executor.test.js +33 -0
  373. package/dist/test/belayer-intake.test.js +44 -0
  374. package/dist/test/belayer-pipeline.test.js +113 -0
  375. package/dist/test/belayer-pr-lifecycle.test.js +26 -0
  376. package/dist/test/belayer-prompts.test.js +60 -0
  377. package/dist/test/belayer-types.test.js +69 -0
  378. package/dist/test/bin/claude-remote-cli.js +214 -0
  379. package/dist/test/boot-state.test.js +133 -0
  380. package/dist/test/branch-lifecycle.test.js +75 -0
  381. package/dist/test/branch-linker.test.js +236 -0
  382. package/dist/test/branch-rename.test.js +45 -0
  383. package/dist/test/branch-watcher.test.js +115 -0
  384. package/dist/test/browser-cli.test.js +91 -0
  385. package/dist/test/browser-content.test.js +93 -0
  386. package/dist/test/browser-tabs-ui.test.js +39 -0
  387. package/dist/test/changed-files-api.test.js +140 -0
  388. package/dist/test/clipboard.test.js +12 -0
  389. package/dist/test/codex-hooks-adapter.test.js +237 -0
  390. package/dist/test/components/EmptyState.spec.js +51 -0
  391. package/dist/test/components/ErrorToast.spec.js +65 -0
  392. package/dist/test/components/TuiCheckbox.spec.js +120 -0
  393. package/dist/test/components/TuiInput.spec.js +186 -0
  394. package/dist/test/components/leaf-component-migration.spec.js +104 -0
  395. package/dist/test/config-freshness.test.js +63 -0
  396. package/dist/test/config.test.js +813 -0
  397. package/dist/test/diff-summary.test.js +98 -0
  398. package/dist/test/display-state.test.js +179 -0
  399. package/dist/test/event-message-types.test.js +32 -0
  400. package/dist/test/file-tree-utils.test.js +167 -0
  401. package/dist/test/framework-types.test.js +183 -0
  402. package/dist/test/frameworks-api.test.js +93 -0
  403. package/dist/test/frontend/src/lib/pr-state.js +114 -0
  404. package/dist/test/fs-browse.test.js +246 -0
  405. package/dist/test/fuzzy-scorer.test.js +145 -0
  406. package/dist/test/gh-routes.test.js +156 -0
  407. package/dist/test/git-changed-files.test.js +152 -0
  408. package/dist/test/git-routes.test.js +146 -0
  409. package/dist/test/git-utils.test.js +68 -0
  410. package/dist/test/git-watcher.test.js +110 -0
  411. package/dist/test/git.test.js +140 -0
  412. package/dist/test/github-app.test.js +455 -0
  413. package/dist/test/github-graphql.test.js +301 -0
  414. package/dist/test/greetings.test.js +83 -0
  415. package/dist/test/hooks-agent-event.test.js +412 -0
  416. package/dist/test/hooks.test.js +149 -0
  417. package/dist/test/integration-github.test.js +220 -0
  418. package/dist/test/integration-jira.test.js +238 -0
  419. package/dist/test/integration-linear.test.js +293 -0
  420. package/dist/test/mobile-input.test.js +235 -0
  421. package/dist/test/opencode-relay.test.js +107 -0
  422. package/dist/test/org-dashboard.test.js +349 -0
  423. package/dist/test/output-parser.test.js +217 -0
  424. package/dist/test/paths.test.js +32 -0
  425. package/dist/test/pr-state.test.js +407 -0
  426. package/dist/test/pr-status.test.js +82 -0
  427. package/dist/test/presets.test.js +242 -0
  428. package/dist/test/pty-handler-multi-agent.test.js +149 -0
  429. package/dist/test/pty-handler.test.js +146 -0
  430. package/dist/test/pull-requests.test.js +78 -0
  431. package/dist/test/review-poller.test.js +349 -0
  432. package/dist/test/server/analytics.js +121 -0
  433. package/dist/test/server/auth.js +63 -0
  434. package/dist/test/server/branch-linker.js +124 -0
  435. package/dist/test/server/clipboard.js +56 -0
  436. package/dist/test/server/config.js +137 -0
  437. package/dist/test/server/git.js +308 -0
  438. package/dist/test/server/hooks.js +196 -0
  439. package/dist/test/server/index.js +1124 -0
  440. package/dist/test/server/integration-github.js +117 -0
  441. package/dist/test/server/integration-jira.js +164 -0
  442. package/dist/test/server/integration-linear.js +176 -0
  443. package/dist/test/server/mobile-input-pipeline.js +123 -0
  444. package/dist/test/server/org-dashboard.js +184 -0
  445. package/dist/test/server/output-parsers/claude-parser.js +54 -0
  446. package/dist/test/server/output-parsers/codex-parser.js +13 -0
  447. package/dist/test/server/output-parsers/index.js +7 -0
  448. package/dist/test/server/pty-handler.js +310 -0
  449. package/dist/test/server/push.js +80 -0
  450. package/dist/test/server/review-poller.js +218 -0
  451. package/dist/test/server/service.js +169 -0
  452. package/dist/test/server/sessions.js +434 -0
  453. package/dist/test/server/ticket-transitions.js +216 -0
  454. package/dist/test/server/types.js +20 -0
  455. package/dist/test/server/utils.js +22 -0
  456. package/dist/test/server/watcher.js +139 -0
  457. package/dist/test/server/workspaces.js +657 -0
  458. package/dist/test/server/ws.js +152 -0
  459. package/dist/test/server-startup.test.js +62 -0
  460. package/dist/test/service.test.js +43 -0
  461. package/dist/test/session-analytics-api.test.js +123 -0
  462. package/dist/test/session-analytics.test.js +425 -0
  463. package/dist/test/session-intent.test.js +249 -0
  464. package/dist/test/sessions.test.js +1152 -0
  465. package/dist/test/sidebar-items.test.js +164 -0
  466. package/dist/test/stores/boot-state-store.test.js +165 -0
  467. package/dist/test/stores/sessions-logic.test.js +191 -0
  468. package/dist/test/stores/toasts-store.test.js +66 -0
  469. package/dist/test/stores/ui-store.test.js +203 -0
  470. package/dist/test/stores/unread-store.test.js +97 -0
  471. package/dist/test/telemetry-api.test.js +54 -0
  472. package/dist/test/telemetry-sync.test.js +68 -0
  473. package/dist/test/telemetry.test.js +295 -0
  474. package/dist/test/terminal-zoom.test.js +102 -0
  475. package/dist/test/test/analytics.test.js +152 -0
  476. package/dist/test/test/auth.test.js +95 -0
  477. package/dist/test/test/branch-linker.test.js +231 -0
  478. package/dist/test/test/branch-rename.test.js +45 -0
  479. package/dist/test/test/clipboard.test.js +12 -0
  480. package/dist/test/test/config.test.js +281 -0
  481. package/dist/test/test/fs-browse.test.js +202 -0
  482. package/dist/test/test/git.test.js +67 -0
  483. package/dist/test/test/hooks.test.js +139 -0
  484. package/dist/test/test/integration-github.test.js +203 -0
  485. package/dist/test/test/integration-jira.test.js +294 -0
  486. package/dist/test/test/integration-linear.test.js +293 -0
  487. package/dist/test/test/mobile-input.test.js +193 -0
  488. package/dist/test/test/org-dashboard.test.js +240 -0
  489. package/dist/test/test/output-parser.test.js +95 -0
  490. package/dist/test/test/paths.test.js +32 -0
  491. package/dist/test/test/pr-state.test.js +220 -0
  492. package/dist/test/test/pull-requests.test.js +67 -0
  493. package/dist/test/test/review-poller.test.js +235 -0
  494. package/dist/test/test/service.test.js +43 -0
  495. package/dist/test/test/sessions.test.js +750 -0
  496. package/dist/test/test/ticket-transitions.test.js +130 -0
  497. package/dist/test/test/version.test.js +34 -0
  498. package/dist/test/test/worktrees.test.js +256 -0
  499. package/dist/test/ticket-transitions.test.js +312 -0
  500. package/dist/test/unread.test.js +23 -0
  501. package/dist/test/version.test.js +34 -0
  502. package/dist/test/webhook-manager.test.js +484 -0
  503. package/dist/test/webhooks.test.js +208 -0
  504. package/dist/test/workspace-groups.test.js +377 -0
  505. package/dist/test/worktrees.test.js +531 -0
  506. package/package.json +88 -0
@@ -0,0 +1,1124 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import readline from 'node:readline';
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import express from 'express';
10
+ import cookieParser from 'cookie-parser';
11
+ import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir, resolveSessionSettings } from './config.js';
12
+ import * as auth from './auth.js';
13
+ import * as sessions from './sessions.js';
14
+ import { AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS, serializeAll, restoreFromDisk, activeTmuxSessionNames, populateMetaCache } from './sessions.js';
15
+ import { setupWebSocket } from './ws.js';
16
+ import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
17
+ import { isInstalled as serviceIsInstalled } from './service.js';
18
+ import { extensionForMime, setClipboardImage } from './clipboard.js';
19
+ import { listBranches, isBranchStale } from './git.js';
20
+ import * as push from './push.js';
21
+ import { initAnalytics, closeAnalytics, createAnalyticsRouter } from './analytics.js';
22
+ import { createWorkspaceRouter } from './workspaces.js';
23
+ import { createOrgDashboardRouter } from './org-dashboard.js';
24
+ import { createIntegrationGitHubRouter } from './integration-github.js';
25
+ import { createBranchLinkerRouter, invalidateBranchLinkerCache } from './branch-linker.js';
26
+ import { createHooksRouter } from './hooks.js';
27
+ import { createTicketTransitionsRouter } from './ticket-transitions.js';
28
+ import { createIntegrationJiraRouter } from './integration-jira.js';
29
+ import { createIntegrationLinearRouter } from './integration-linear.js';
30
+ import { startPolling, stopPolling } from './review-poller.js';
31
+ import { MOUNTAIN_NAMES } from './types.js';
32
+ import { semverLessThan } from './utils.js';
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = path.dirname(__filename);
35
+ const execFileAsync = promisify(execFile);
36
+ // When run via CLI bin, config lives in ~/.config/claude-remote-cli/
37
+ // When run directly (development), fall back to local config.json
38
+ const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
39
+ const VERSION_CACHE_TTL = 5 * 60 * 1000;
40
+ let versionCache = null;
41
+ function getCurrentVersion() {
42
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
43
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
44
+ return pkg.version;
45
+ }
46
+ async function getLatestVersion() {
47
+ const now = Date.now();
48
+ if (versionCache && now - versionCache.fetchedAt < VERSION_CACHE_TTL) {
49
+ return versionCache.latest;
50
+ }
51
+ try {
52
+ const res = await fetch('https://registry.npmjs.org/claude-remote-cli/latest');
53
+ if (!res.ok)
54
+ return null;
55
+ const data = await res.json();
56
+ if (!data.version)
57
+ return null;
58
+ versionCache = { latest: data.version, fetchedAt: now };
59
+ return data.version;
60
+ }
61
+ catch (_) {
62
+ return null;
63
+ }
64
+ }
65
+ function execErrorMessage(err, fallback) {
66
+ const e = err;
67
+ return (e.stderr || e.message || fallback).trim();
68
+ }
69
+ function scanReposInRoot(rootDir) {
70
+ const repos = [];
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
74
+ }
75
+ catch (_) {
76
+ return repos;
77
+ }
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
80
+ continue;
81
+ const fullPath = path.join(rootDir, entry.name);
82
+ const dotGit = path.join(fullPath, '.git');
83
+ try {
84
+ if (fs.statSync(dotGit).isDirectory()) {
85
+ repos.push({ name: entry.name, path: fullPath, root: rootDir });
86
+ }
87
+ }
88
+ catch (_) {
89
+ // .git doesn't exist — not a repo
90
+ }
91
+ }
92
+ return repos;
93
+ }
94
+ function scanAllRepos(rootDirs) {
95
+ const repos = [];
96
+ for (const rootDir of rootDirs) {
97
+ repos.push(...scanReposInRoot(rootDir));
98
+ }
99
+ return repos;
100
+ }
101
+ function parseTTL(ttl) {
102
+ if (typeof ttl !== 'string')
103
+ return 24 * 60 * 60 * 1000;
104
+ const match = ttl.match(/^(\d+)([smhd])$/);
105
+ if (!match)
106
+ return 24 * 60 * 60 * 1000;
107
+ const value = parseInt(match[1], 10);
108
+ switch (match[2]) {
109
+ case 's': return value * 1000;
110
+ case 'm': return value * 60 * 1000;
111
+ case 'h': return value * 60 * 60 * 1000;
112
+ case 'd': return value * 24 * 60 * 60 * 1000;
113
+ default: return 24 * 60 * 60 * 1000;
114
+ }
115
+ }
116
+ function promptPin(question) {
117
+ return new Promise((resolve) => {
118
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
119
+ rl.question(question, (answer) => {
120
+ rl.close();
121
+ resolve(answer.trim());
122
+ });
123
+ });
124
+ }
125
+ function ensureGitignore(repoPath, entry) {
126
+ const gitignorePath = path.join(repoPath, '.gitignore');
127
+ try {
128
+ if (fs.existsSync(gitignorePath)) {
129
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
130
+ if (content.split('\n').some((line) => line.trim() === entry))
131
+ return;
132
+ const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
133
+ fs.appendFileSync(gitignorePath, prefix + entry + '\n');
134
+ }
135
+ else {
136
+ fs.writeFileSync(gitignorePath, entry + '\n');
137
+ }
138
+ }
139
+ catch (_) {
140
+ // Non-fatal: gitignore update failure shouldn't block session creation
141
+ }
142
+ }
143
+ async function main() {
144
+ // Ignore SIGPIPE: node-pty can propagate pipe breaks causing unexpected session exits
145
+ process.on('SIGPIPE', () => { });
146
+ // Ignore SIGHUP: keep server alive if controlling terminal disconnects
147
+ process.on('SIGHUP', () => { });
148
+ ensureMetaDir(CONFIG_PATH);
149
+ let config;
150
+ try {
151
+ config = loadConfig(CONFIG_PATH);
152
+ }
153
+ catch (_) {
154
+ config = { ...DEFAULTS };
155
+ saveConfig(CONFIG_PATH, config);
156
+ }
157
+ // CLI flag overrides
158
+ if (process.env.CLAUDE_REMOTE_PORT)
159
+ config.port = parseInt(process.env.CLAUDE_REMOTE_PORT, 10);
160
+ if (process.env.CLAUDE_REMOTE_HOST)
161
+ config.host = process.env.CLAUDE_REMOTE_HOST;
162
+ push.ensureVapidKeys(config, CONFIG_PATH, saveConfig);
163
+ const configDir = path.dirname(CONFIG_PATH);
164
+ try {
165
+ initAnalytics(configDir);
166
+ }
167
+ catch (err) {
168
+ console.warn('Analytics disabled: failed to initialize:', err instanceof Error ? err.message : err);
169
+ }
170
+ if (config.pinHash && auth.isLegacyHash(config.pinHash)) {
171
+ console.log('Migrating legacy PIN hash to scrypt. You will need to set a new PIN.');
172
+ delete config.pinHash;
173
+ saveConfig(CONFIG_PATH, config);
174
+ }
175
+ if (!config.pinHash) {
176
+ if (!process.stdin.isTTY) {
177
+ console.error('No PIN configured. Run claude-remote-cli interactively first to set a PIN.');
178
+ process.exit(1);
179
+ }
180
+ const pin = await promptPin('Set up a PIN for claude-remote-cli:');
181
+ config.pinHash = await auth.hashPin(pin);
182
+ saveConfig(CONFIG_PATH, config);
183
+ console.log('PIN set successfully.');
184
+ }
185
+ const authenticatedTokens = new Set();
186
+ // Build frontend if missing (e.g. fresh clone in development)
187
+ const frontendDir = path.join(__dirname, '..', 'frontend');
188
+ if (!fs.existsSync(path.join(frontendDir, 'index.html'))) {
189
+ const packageRoot = path.join(__dirname, '..', '..');
190
+ const viteConfig = path.join(packageRoot, 'frontend', 'vite.config.ts');
191
+ if (fs.existsSync(viteConfig)) {
192
+ console.log('Frontend not built — building now...');
193
+ try {
194
+ await execFileAsync('npx', ['vite', 'build', '--config', 'frontend/vite.config.ts'], { cwd: packageRoot });
195
+ console.log('Frontend build complete.');
196
+ }
197
+ catch (err) {
198
+ console.error('Frontend build failed:', err instanceof Error ? err.message : err);
199
+ }
200
+ }
201
+ else {
202
+ console.warn('Frontend assets missing and source not available — UI will not be served.');
203
+ }
204
+ }
205
+ const app = express();
206
+ app.use(express.json({ limit: '15mb' }));
207
+ app.use(cookieParser());
208
+ app.use(express.static(frontendDir));
209
+ const requireAuth = (req, res, next) => {
210
+ const token = req.cookies && req.cookies.token;
211
+ if (!token || !authenticatedTokens.has(token)) {
212
+ res.status(401).json({ error: 'Unauthorized' });
213
+ return;
214
+ }
215
+ next();
216
+ };
217
+ function boolConfigEndpoints(name, defaultValue, onEnable) {
218
+ app.get(`/config/${name}`, requireAuth, (_req, res) => {
219
+ res.json({ [name]: config[name] ?? defaultValue });
220
+ });
221
+ app.patch(`/config/${name}`, requireAuth, async (req, res) => {
222
+ const value = req.body[name];
223
+ if (typeof value !== 'boolean') {
224
+ res.status(400).json({ error: `${name} must be a boolean` });
225
+ return;
226
+ }
227
+ if (value && onEnable) {
228
+ try {
229
+ await onEnable();
230
+ }
231
+ catch {
232
+ res.status(400).json({ error: `Validation failed for ${name}` });
233
+ return;
234
+ }
235
+ }
236
+ config[name] = value;
237
+ saveConfig(CONFIG_PATH, config);
238
+ res.json({ [name]: value });
239
+ });
240
+ }
241
+ const watcher = new WorktreeWatcher();
242
+ watcher.rebuild(config.workspaces || []);
243
+ const server = http.createServer(app);
244
+ const { broadcastEvent } = setupWebSocket(server, authenticatedTokens, watcher, CONFIG_PATH);
245
+ // Configure session defaults for hooks injection
246
+ sessions.configure({ port: config.port, forceOutputParser: config.forceOutputParser ?? false });
247
+ // Mount hooks router BEFORE auth middleware — hook callbacks come from localhost Claude Code
248
+ const hooksRouter = createHooksRouter({
249
+ getSession: sessions.get,
250
+ broadcastEvent,
251
+ fireStateChange: sessions.fireStateChange,
252
+ notifySessionAttention: push.notifySessionAttention,
253
+ configPath: CONFIG_PATH,
254
+ });
255
+ app.use('/hooks', hooksRouter);
256
+ // Mount workspace router
257
+ const workspaceRouter = createWorkspaceRouter({ configPath: CONFIG_PATH });
258
+ app.use('/workspaces', requireAuth, workspaceRouter);
259
+ // Mount GitHub integration router
260
+ const integrationGitHubRouter = createIntegrationGitHubRouter({ configPath: CONFIG_PATH });
261
+ app.use('/integration-github', requireAuth, integrationGitHubRouter);
262
+ // Mount Jira integration router
263
+ const integrationJiraRouter = createIntegrationJiraRouter({ configPath: CONFIG_PATH });
264
+ app.use('/integration-jira', requireAuth, integrationJiraRouter);
265
+ // Mount Linear integration router
266
+ const integrationLinearRouter = createIntegrationLinearRouter({ configPath: CONFIG_PATH });
267
+ app.use('/integration-linear', requireAuth, integrationLinearRouter);
268
+ // Mount branch linker router
269
+ const branchLinkerRouter = createBranchLinkerRouter({
270
+ configPath: CONFIG_PATH,
271
+ getActiveBranchNames: () => {
272
+ const map = new Map();
273
+ for (const s of sessions.list()) {
274
+ if (!s.branchName)
275
+ continue;
276
+ const existing = map.get(s.repoPath);
277
+ if (existing) {
278
+ existing.add(s.branchName);
279
+ }
280
+ else {
281
+ map.set(s.repoPath, new Set([s.branchName]));
282
+ }
283
+ }
284
+ return map;
285
+ },
286
+ });
287
+ app.use('/branch-linker', requireAuth, branchLinkerRouter);
288
+ // Mount ticket transitions router
289
+ const { router: ticketTransitionsRouter, transitionOnSessionCreate, checkPrTransitions } = createTicketTransitionsRouter({ configPath: CONFIG_PATH });
290
+ app.use('/ticket-transitions', requireAuth, ticketTransitionsRouter);
291
+ // Mount org dashboard router — use branchLinkerRouter.fetchLinks() directly (no loopback HTTP)
292
+ const orgDashboardRouter = createOrgDashboardRouter({ configPath: CONFIG_PATH, checkPrTransitions, getBranchLinks: () => branchLinkerRouter.fetchLinks() });
293
+ app.use('/org-dashboard', requireAuth, orgDashboardRouter);
294
+ // Mount analytics router
295
+ app.use('/analytics', requireAuth, createAnalyticsRouter(configDir));
296
+ // Restore sessions from a previous update restart
297
+ const restoredCount = await restoreFromDisk(configDir);
298
+ if (restoredCount > 0) {
299
+ console.log(`Restored ${restoredCount} session(s) from previous update.`);
300
+ }
301
+ // Populate session metadata cache in background (non-blocking)
302
+ populateMetaCache().catch(() => { });
303
+ // Build shared deps for review poller
304
+ function buildPollerDeps() {
305
+ return {
306
+ configPath: CONFIG_PATH,
307
+ getWorkspacePaths: () => config.workspaces ?? [],
308
+ getWorkspaceSettings: (wsPath) => config.workspaceSettings?.[wsPath],
309
+ createSession: async (opts) => {
310
+ const resolved = resolveSessionSettings(config, opts.repoPath, {});
311
+ const roots = config.rootDirs || [];
312
+ const root = roots.find((r) => opts.repoPath.startsWith(r)) || '';
313
+ const repoName = opts.repoPath.split('/').filter(Boolean).pop() || 'session';
314
+ const worktreeName = opts.worktreePath.split('/').pop() || '';
315
+ const displayName = sessions.nextAgentName();
316
+ sessions.create({
317
+ type: 'worktree',
318
+ agent: resolved.agent,
319
+ repoName,
320
+ repoPath: opts.worktreePath,
321
+ cwd: opts.worktreePath,
322
+ root,
323
+ worktreeName,
324
+ branchName: opts.branchName,
325
+ displayName,
326
+ args: [...resolved.claudeArgs, ...(resolved.yolo ? AGENT_YOLO_ARGS[resolved.agent] : [])],
327
+ configPath: CONFIG_PATH,
328
+ useTmux: resolved.useTmux,
329
+ ...(opts.initialPrompt != null && { initialPrompt: opts.initialPrompt }),
330
+ });
331
+ },
332
+ broadcastEvent,
333
+ };
334
+ }
335
+ // Start review request poller if enabled
336
+ if (config.automations?.autoCheckoutReviewRequests) {
337
+ startPolling(buildPollerDeps());
338
+ }
339
+ // Invalidate branch linker cache on session lifecycle changes
340
+ sessions.onSessionCreate(() => { invalidateBranchLinkerCache(); });
341
+ sessions.onSessionEnd(() => { invalidateBranchLinkerCache(); });
342
+ // Push notifications on session idle (skip when hooks already sent attention notification)
343
+ sessions.onIdleChange((sessionId, idle) => {
344
+ if (idle) {
345
+ const session = sessions.get(sessionId);
346
+ if (session && session.type !== 'terminal') {
347
+ // Dedup: if hooks fired an attention notification within last 10s, skip
348
+ if (session.hooksActive && session.lastAttentionNotifiedAt && Date.now() - session.lastAttentionNotifiedAt < 10000) {
349
+ return;
350
+ }
351
+ push.notifySessionAttention(sessionId, session);
352
+ }
353
+ }
354
+ });
355
+ // POST /auth
356
+ app.post('/auth', async (req, res) => {
357
+ const ip = (req.ip || req.connection.remoteAddress);
358
+ if (auth.isRateLimited(ip)) {
359
+ res.status(429).json({ error: 'Too many attempts. Try again later.' });
360
+ return;
361
+ }
362
+ const { pin } = req.body;
363
+ if (!pin) {
364
+ res.status(400).json({ error: 'PIN required' });
365
+ return;
366
+ }
367
+ const valid = await auth.verifyPin(pin, config.pinHash);
368
+ if (!valid) {
369
+ auth.recordFailedAttempt(ip);
370
+ res.status(401).json({ error: 'Invalid PIN' });
371
+ return;
372
+ }
373
+ auth.clearRateLimit(ip);
374
+ const token = auth.generateCookieToken();
375
+ authenticatedTokens.add(token);
376
+ const ttlMs = parseTTL(config.cookieTTL);
377
+ setTimeout(() => authenticatedTokens.delete(token), ttlMs);
378
+ res.cookie('token', token, {
379
+ httpOnly: true,
380
+ sameSite: 'strict',
381
+ maxAge: ttlMs,
382
+ });
383
+ res.json({ ok: true });
384
+ });
385
+ // GET /sessions
386
+ app.get('/sessions', requireAuth, (_req, res) => {
387
+ res.json(sessions.list());
388
+ });
389
+ // GET /repos — scan root dirs for repos
390
+ app.get('/repos', requireAuth, async (_req, res) => {
391
+ const repos = scanAllRepos(config.rootDirs || []);
392
+ // Also include legacy manually-added repos
393
+ if (config.repos) {
394
+ for (const repo of config.repos) {
395
+ if (!repos.some((r) => r.path === repo.path)) {
396
+ repos.push(repo);
397
+ }
398
+ }
399
+ }
400
+ // Enrich with current branch (best-effort, parallel)
401
+ const enriched = await Promise.all(repos.map(async (repo) => {
402
+ try {
403
+ const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: repo.path });
404
+ return { ...repo, defaultBranch: stdout.trim() };
405
+ }
406
+ catch {
407
+ return { ...repo, defaultBranch: null };
408
+ }
409
+ }));
410
+ res.json(enriched);
411
+ });
412
+ // GET /branches?repo=<path> — list local and remote branches for a repo
413
+ app.get('/branches', requireAuth, async (req, res) => {
414
+ const repoPath = typeof req.query.repo === 'string' ? req.query.repo : undefined;
415
+ const refresh = req.query.refresh === '1';
416
+ if (!repoPath) {
417
+ res.status(400).json({ error: 'repo query parameter is required' });
418
+ return;
419
+ }
420
+ res.json(await listBranches(repoPath, { refresh }));
421
+ });
422
+ // GET /worktrees?repo=<path> — list worktrees; omit repo to scan all repos in all rootDirs
423
+ app.get('/worktrees', requireAuth, async (req, res) => {
424
+ const repoParam = typeof req.query.repo === 'string' ? req.query.repo : undefined;
425
+ const roots = config.rootDirs || [];
426
+ const worktrees = [];
427
+ let reposToScan;
428
+ if (repoParam) {
429
+ const root = roots.find(function (r) { return repoParam.startsWith(r); }) || '';
430
+ reposToScan = [{ path: repoParam, name: repoParam.split('/').filter(Boolean).pop() || '', root }];
431
+ }
432
+ else {
433
+ reposToScan = [];
434
+ for (const rootDir of roots) {
435
+ let entries;
436
+ try {
437
+ entries = fs.readdirSync(rootDir, { withFileTypes: true });
438
+ }
439
+ catch (_) {
440
+ continue;
441
+ }
442
+ for (const entry of entries) {
443
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
444
+ continue;
445
+ const fullPath = path.join(rootDir, entry.name);
446
+ const dotGit = path.join(fullPath, '.git');
447
+ try {
448
+ if (fs.statSync(dotGit).isDirectory()) {
449
+ reposToScan.push({ name: entry.name, path: fullPath, root: rootDir });
450
+ }
451
+ }
452
+ catch (_) {
453
+ // .git doesn't exist — not a repo
454
+ }
455
+ }
456
+ }
457
+ }
458
+ for (const repo of reposToScan) {
459
+ // Use git worktree list to discover all worktrees (including those at arbitrary paths)
460
+ try {
461
+ const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repo.path });
462
+ const parsed = parseWorktreeListPorcelain(stdout, repo.path);
463
+ for (const wt of parsed) {
464
+ const dirName = wt.path.split('/').pop() || '';
465
+ const meta = readMeta(CONFIG_PATH, wt.path);
466
+ worktrees.push({
467
+ name: dirName,
468
+ path: wt.path,
469
+ repoName: repo.name,
470
+ repoPath: repo.path,
471
+ root: repo.root,
472
+ displayName: meta?.displayName || wt.branch || dirName,
473
+ lastActivity: meta?.lastActivity || '',
474
+ branchName: wt.branch || meta?.branchName || dirName,
475
+ });
476
+ }
477
+ }
478
+ catch {
479
+ // git worktree list failed — fall back to directory scanning
480
+ for (const dir of WORKTREE_DIRS) {
481
+ const worktreeDir = path.join(repo.path, dir);
482
+ let entries;
483
+ try {
484
+ entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
485
+ }
486
+ catch (_) {
487
+ continue;
488
+ }
489
+ for (const entry of entries) {
490
+ if (!entry.isDirectory())
491
+ continue;
492
+ const wtPath = path.join(worktreeDir, entry.name);
493
+ const meta = readMeta(CONFIG_PATH, wtPath);
494
+ worktrees.push({
495
+ name: entry.name,
496
+ path: wtPath,
497
+ repoName: repo.name,
498
+ repoPath: repo.path,
499
+ root: repo.root,
500
+ displayName: meta?.displayName || '',
501
+ lastActivity: meta?.lastActivity || '',
502
+ branchName: meta?.branchName || entry.name,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+ // Deduplicate by path (a worktree can appear via multiple repo scans)
509
+ const seen = new Set();
510
+ const unique = worktrees.filter(wt => {
511
+ if (seen.has(wt.path))
512
+ return false;
513
+ seen.add(wt.path);
514
+ return true;
515
+ });
516
+ res.json(unique);
517
+ });
518
+ // GET /config/defaultAgent — get default coding agent
519
+ app.get('/config/defaultAgent', requireAuth, (_req, res) => {
520
+ res.json({ defaultAgent: config.defaultAgent || 'claude' });
521
+ });
522
+ // PATCH /config/defaultAgent — set default coding agent
523
+ app.patch('/config/defaultAgent', requireAuth, (req, res) => {
524
+ const { defaultAgent } = req.body;
525
+ if (!defaultAgent || (defaultAgent !== 'claude' && defaultAgent !== 'codex')) {
526
+ res.status(400).json({ error: 'defaultAgent must be "claude" or "codex"' });
527
+ return;
528
+ }
529
+ config.defaultAgent = defaultAgent;
530
+ saveConfig(CONFIG_PATH, config);
531
+ res.json({ defaultAgent: config.defaultAgent });
532
+ });
533
+ boolConfigEndpoints('defaultContinue', true);
534
+ boolConfigEndpoints('defaultYolo', false);
535
+ boolConfigEndpoints('launchInTmux', false, async () => {
536
+ await execFileAsync('tmux', ['-V']);
537
+ });
538
+ boolConfigEndpoints('defaultNotifications', true);
539
+ // GET /config/automations — get automation settings
540
+ app.get('/config/automations', requireAuth, (_req, res) => {
541
+ res.json(config.automations ?? {});
542
+ });
543
+ // PATCH /config/automations — update automation settings and start/stop poller
544
+ app.patch('/config/automations', requireAuth, (req, res) => {
545
+ const body = req.body;
546
+ const prev = config.automations ?? {};
547
+ const next = { ...prev };
548
+ if (typeof body.autoCheckoutReviewRequests === 'boolean') {
549
+ next.autoCheckoutReviewRequests = body.autoCheckoutReviewRequests;
550
+ }
551
+ if (typeof body.autoReviewOnCheckout === 'boolean') {
552
+ next.autoReviewOnCheckout = body.autoReviewOnCheckout;
553
+ }
554
+ if (typeof body.pollIntervalMs === 'number' && body.pollIntervalMs >= 60000) {
555
+ next.pollIntervalMs = body.pollIntervalMs;
556
+ }
557
+ // Enforce: auto-review requires auto-checkout
558
+ if (!next.autoCheckoutReviewRequests) {
559
+ next.autoReviewOnCheckout = false;
560
+ }
561
+ config.automations = next;
562
+ saveConfig(CONFIG_PATH, config);
563
+ // Start or stop poller based on new setting
564
+ if (next.autoCheckoutReviewRequests) {
565
+ stopPolling();
566
+ startPolling(buildPollerDeps());
567
+ }
568
+ else {
569
+ stopPolling();
570
+ }
571
+ res.json(next);
572
+ });
573
+ // GET /config/workspace-groups — return workspace group configuration
574
+ app.get('/config/workspace-groups', requireAuth, (_req, res) => {
575
+ res.json({ groups: config.workspaceGroups ?? {} });
576
+ });
577
+ // GET /push/vapid-key
578
+ app.get('/push/vapid-key', requireAuth, (_req, res) => {
579
+ const key = push.getVapidPublicKey();
580
+ if (!key) {
581
+ res.status(501).json({ error: 'Push not available' });
582
+ return;
583
+ }
584
+ res.json({ vapidPublicKey: key });
585
+ });
586
+ // POST /push/subscribe
587
+ app.post('/push/subscribe', requireAuth, (req, res) => {
588
+ const { subscription, sessionIds } = req.body;
589
+ if (!subscription?.endpoint) {
590
+ res.status(400).json({ error: 'subscription required' });
591
+ return;
592
+ }
593
+ push.subscribe(subscription, sessionIds || []);
594
+ res.json({ ok: true });
595
+ });
596
+ // POST /push/unsubscribe
597
+ app.post('/push/unsubscribe', requireAuth, (req, res) => {
598
+ const { endpoint } = req.body;
599
+ if (!endpoint) {
600
+ res.status(400).json({ error: 'endpoint required' });
601
+ return;
602
+ }
603
+ push.unsubscribe(endpoint);
604
+ res.json({ ok: true });
605
+ });
606
+ // DELETE /worktrees — remove a worktree, prune, and delete its branch
607
+ app.delete('/worktrees', requireAuth, async (req, res) => {
608
+ const { worktreePath, repoPath } = req.body;
609
+ if (!worktreePath || !repoPath) {
610
+ res.status(400).json({ error: 'worktreePath and repoPath are required' });
611
+ return;
612
+ }
613
+ // Validate the path is a real git worktree (not the main worktree)
614
+ try {
615
+ const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
616
+ const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
617
+ const isKnownWorktree = allWorktrees.some(wt => wt.path === path.resolve(worktreePath) && !wt.isMain);
618
+ if (!isKnownWorktree) {
619
+ res.status(400).json({ error: 'Path is not a recognized git worktree' });
620
+ return;
621
+ }
622
+ }
623
+ catch {
624
+ // If git worktree list fails, fall back to the directory-name check
625
+ if (!isValidWorktreePath(worktreePath)) {
626
+ res.status(400).json({ error: 'Path is not inside a worktree directory' });
627
+ return;
628
+ }
629
+ }
630
+ // Multiple sessions per worktree allowed (multi-tab support)
631
+ // Derive branch name from metadata or worktree directory name
632
+ const meta = readMeta(CONFIG_PATH, worktreePath);
633
+ const branchName = (meta && meta.branchName) || worktreePath.split('/').pop() || '';
634
+ try {
635
+ // Will fail if uncommitted changes -- no --force
636
+ await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
637
+ }
638
+ catch (err) {
639
+ // If git worktree remove fails, the directory may be an orphaned worktree
640
+ // that git no longer tracks. Try to remove the directory directly.
641
+ if (fs.existsSync(worktreePath)) {
642
+ try {
643
+ fs.rmSync(worktreePath, { recursive: true });
644
+ }
645
+ catch (rmErr) {
646
+ res.status(500).json({ error: execErrorMessage(rmErr, 'Failed to remove worktree directory') });
647
+ return;
648
+ }
649
+ }
650
+ // If directory doesn't exist, the worktree is already gone — continue to cleanup
651
+ }
652
+ try {
653
+ // Prune stale worktree refs
654
+ await execFileAsync('git', ['worktree', 'prune'], { cwd: repoPath });
655
+ }
656
+ catch (_) {
657
+ // Non-fatal: prune failure doesn't block success
658
+ }
659
+ if (branchName) {
660
+ try {
661
+ // Delete the branch
662
+ await execFileAsync('git', ['branch', '-D', branchName], { cwd: repoPath });
663
+ }
664
+ catch (_) {
665
+ // Non-fatal: branch may not exist or may be checked out elsewhere
666
+ }
667
+ }
668
+ // Clean up metadata file
669
+ deleteMeta(CONFIG_PATH, worktreePath);
670
+ res.json({ ok: true });
671
+ });
672
+ // POST /sessions
673
+ app.post('/sessions', requireAuth, async (req, res) => {
674
+ const { repoPath, repoName, worktreePath, branchName, claudeArgs, yolo, agent, useTmux, cols, rows, needsBranchRename, branchRenamePrompt, ticketContext } = req.body;
675
+ if (!repoPath) {
676
+ res.status(400).json({ error: 'repoPath is required' });
677
+ return;
678
+ }
679
+ // Sanitize optional terminal dimensions
680
+ const safeCols = typeof cols === 'number' && Number.isFinite(cols) && cols >= 1 && cols <= 500 ? Math.round(cols) : undefined;
681
+ const safeRows = typeof rows === 'number' && Number.isFinite(rows) && rows >= 1 && rows <= 200 ? Math.round(rows) : undefined;
682
+ const resolved = resolveSessionSettings(config, repoPath, { agent, yolo, useTmux, claudeArgs });
683
+ const resolvedAgent = resolved.agent;
684
+ const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
685
+ let initialPrompt;
686
+ if (ticketContext && (typeof ticketContext.ticketId !== 'string' || typeof ticketContext.title !== 'string' || typeof ticketContext.url !== 'string')) {
687
+ res.status(400).json({ error: 'ticketContext requires string ticketId, title, and url' });
688
+ return;
689
+ }
690
+ if (ticketContext) {
691
+ // Use ticketContext.repoPath (workspace root) for settings lookup
692
+ const settings = config.workspaceSettings?.[ticketContext.repoPath];
693
+ const template = settings?.promptStartWork ??
694
+ 'You are working on ticket {ticketId}: {title}\n\nTicket URL: {ticketUrl}\n\nPlease start by understanding the issue and proposing an approach.';
695
+ initialPrompt = template
696
+ .replace(/\{ticketId\}/g, ticketContext.ticketId)
697
+ .replace(/\{title\}/g, ticketContext.title)
698
+ .replace(/\{ticketUrl\}/g, ticketContext.url)
699
+ .replace(/\{description\}/g, ticketContext.description ?? '');
700
+ }
701
+ const baseArgs = [
702
+ ...(resolved.claudeArgs),
703
+ ...(resolved.yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
704
+ ];
705
+ // Compute root by matching repoPath against configured rootDirs
706
+ const roots = config.rootDirs || [];
707
+ const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
708
+ let args;
709
+ let cwd;
710
+ let worktreeName;
711
+ let sessionRepoPath;
712
+ let resolvedBranch = '';
713
+ let isMountainName = false;
714
+ if (worktreePath) {
715
+ // Check if the worktree's branch is stale (merged/at base) and needs a fresh name
716
+ const currentBranchResult = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: worktreePath }).catch(() => null);
717
+ const currentBranch = currentBranchResult?.stdout.trim();
718
+ if (currentBranch && !needsBranchRename) {
719
+ const stale = await isBranchStale(worktreePath, currentBranch);
720
+ if (stale) {
721
+ // Generate unique temp branch: <mountain>-<short-timestamp>
722
+ const mountainName = worktreePath.split('/').pop() || 'branch';
723
+ const suffix = Date.now().toString(36).slice(-4);
724
+ const tempBranch = `${mountainName}-${suffix}`;
725
+ try {
726
+ await execFileAsync('git', ['checkout', '-b', tempBranch], { cwd: worktreePath });
727
+ }
728
+ catch {
729
+ await execFileAsync('git', ['branch', '-m', tempBranch], { cwd: worktreePath }).catch(() => { });
730
+ }
731
+ isMountainName = true;
732
+ }
733
+ }
734
+ // Only use --continue if:
735
+ // 1. Not a brand-new worktree (needsBranchRename flag)
736
+ // 2. A prior Claude session exists in this directory (.claude/ dir present)
737
+ // 3. Branch is not stale (isMountainName means we just created a fresh branch)
738
+ const hasPriorSession = !needsBranchRename && !isMountainName && fs.existsSync(path.join(worktreePath, '.claude'));
739
+ args = hasPriorSession ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
740
+ cwd = worktreePath;
741
+ sessionRepoPath = worktreePath;
742
+ worktreeName = worktreePath.split('/').pop() || '';
743
+ }
744
+ else {
745
+ // Create new worktree via git
746
+ let dirName;
747
+ if (branchName) {
748
+ dirName = branchName.replace(/\//g, '-');
749
+ resolvedBranch = branchName;
750
+ }
751
+ else {
752
+ // Pick the next mountain name from the cycling list
753
+ const idx = config.nextMountainIndex || 0;
754
+ const picked = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
755
+ dirName = picked;
756
+ resolvedBranch = picked;
757
+ isMountainName = true;
758
+ config.nextMountainIndex = (idx + 1) % MOUNTAIN_NAMES.length;
759
+ saveConfig(CONFIG_PATH, config);
760
+ }
761
+ const worktreeDir = path.join(repoPath, WORKTREE_DIRS[0]);
762
+ let targetDir = path.join(worktreeDir, dirName);
763
+ if (fs.existsSync(targetDir)) {
764
+ targetDir = targetDir + '-' + Date.now().toString(36);
765
+ dirName = path.basename(targetDir);
766
+ }
767
+ for (const dir of WORKTREE_DIRS) {
768
+ ensureGitignore(repoPath, dir + '/');
769
+ }
770
+ try {
771
+ // Check if branch exists locally or on a remote
772
+ let branchExists = false;
773
+ if (branchName) {
774
+ const localCheck = await execFileAsync('git', ['rev-parse', '--verify', branchName], { cwd: repoPath }).then(() => true, () => false);
775
+ if (localCheck) {
776
+ branchExists = true;
777
+ }
778
+ else {
779
+ const remoteCheck = await execFileAsync('git', ['rev-parse', '--verify', 'origin/' + branchName], { cwd: repoPath }).then(() => true, () => false);
780
+ if (remoteCheck) {
781
+ branchExists = true;
782
+ resolvedBranch = 'origin/' + branchName;
783
+ }
784
+ }
785
+ }
786
+ if (branchName && branchExists) {
787
+ // Check if branch is already checked out in an existing worktree
788
+ const { stdout: wtListOut } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], { cwd: repoPath });
789
+ const allWorktrees = parseAllWorktrees(wtListOut, repoPath);
790
+ const existingWt = allWorktrees.find(wt => wt.branch === branchName);
791
+ if (existingWt) {
792
+ // Branch already checked out — redirect to the existing worktree
793
+ if (existingWt.isMain) {
794
+ // Main worktree → create a repo session
795
+ const existingRepoSession = sessions.findRepoSession(repoPath);
796
+ if (existingRepoSession) {
797
+ res.status(409).json({ error: 'A session already exists for this repo', sessionId: existingRepoSession.id });
798
+ return;
799
+ }
800
+ const repoSession = sessions.create({
801
+ type: 'repo',
802
+ agent: resolvedAgent,
803
+ repoName: name,
804
+ repoPath,
805
+ cwd: repoPath,
806
+ root,
807
+ displayName: sessions.nextAgentName(),
808
+ args: baseArgs,
809
+ useTmux: resolved.useTmux,
810
+ ...(safeCols != null && { cols: safeCols }),
811
+ ...(safeRows != null && { rows: safeRows }),
812
+ ...(initialPrompt != null && { initialPrompt }),
813
+ });
814
+ if (ticketContext) {
815
+ transitionOnSessionCreate(ticketContext).catch((err) => {
816
+ console.error('[index] transition on session create failed:', err);
817
+ });
818
+ }
819
+ res.status(201).json(repoSession);
820
+ return;
821
+ }
822
+ else {
823
+ // Another worktree → create a worktree session with --continue
824
+ cwd = existingWt.path;
825
+ sessionRepoPath = existingWt.path;
826
+ worktreeName = existingWt.path.split('/').pop() || '';
827
+ args = [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs];
828
+ const displayNameVal = sessions.nextAgentName();
829
+ const session = sessions.create({
830
+ type: 'worktree',
831
+ agent: resolvedAgent,
832
+ repoName: name,
833
+ repoPath: sessionRepoPath,
834
+ cwd,
835
+ root,
836
+ worktreeName,
837
+ branchName: branchName || worktreeName,
838
+ displayName: displayNameVal,
839
+ args,
840
+ configPath: CONFIG_PATH,
841
+ useTmux: resolved.useTmux,
842
+ ...(safeCols != null && { cols: safeCols }),
843
+ ...(safeRows != null && { rows: safeRows }),
844
+ ...(initialPrompt != null && { initialPrompt }),
845
+ });
846
+ writeMeta(CONFIG_PATH, {
847
+ worktreePath: sessionRepoPath,
848
+ displayName: displayNameVal,
849
+ lastActivity: new Date().toISOString(),
850
+ branchName: branchName || worktreeName,
851
+ });
852
+ if (ticketContext) {
853
+ transitionOnSessionCreate(ticketContext).catch((err) => {
854
+ console.error('[index] transition on session create failed:', err);
855
+ });
856
+ }
857
+ res.status(201).json(session);
858
+ return;
859
+ }
860
+ }
861
+ await execFileAsync('git', ['worktree', 'add', targetDir, resolvedBranch], { cwd: repoPath });
862
+ }
863
+ else if (branchName) {
864
+ await execFileAsync('git', ['worktree', 'add', '-b', branchName, targetDir, 'HEAD'], { cwd: repoPath });
865
+ }
866
+ else {
867
+ await execFileAsync('git', ['worktree', 'add', '-b', dirName, targetDir, 'HEAD'], { cwd: repoPath });
868
+ }
869
+ }
870
+ catch (err) {
871
+ res.status(500).json({ error: execErrorMessage(err, 'Failed to create worktree') });
872
+ return;
873
+ }
874
+ worktreeName = dirName;
875
+ sessionRepoPath = targetDir;
876
+ cwd = targetDir;
877
+ args = [...baseArgs];
878
+ }
879
+ const displayName = sessions.nextAgentName();
880
+ const session = sessions.create({
881
+ type: 'worktree',
882
+ agent: resolvedAgent,
883
+ repoName: name,
884
+ repoPath: sessionRepoPath,
885
+ cwd,
886
+ root,
887
+ worktreeName,
888
+ branchName: branchName || worktreeName,
889
+ displayName,
890
+ args,
891
+ configPath: CONFIG_PATH,
892
+ useTmux: resolved.useTmux,
893
+ ...(safeCols != null && { cols: safeCols }),
894
+ ...(safeRows != null && { rows: safeRows }),
895
+ needsBranchRename: isMountainName || (needsBranchRename ?? false),
896
+ branchRenamePrompt: branchRenamePrompt ?? '',
897
+ ...(initialPrompt != null && { initialPrompt }),
898
+ });
899
+ if (!worktreePath) {
900
+ writeMeta(CONFIG_PATH, {
901
+ worktreePath: sessionRepoPath,
902
+ displayName,
903
+ lastActivity: new Date().toISOString(),
904
+ branchName: branchName || worktreeName,
905
+ });
906
+ }
907
+ if (ticketContext) {
908
+ transitionOnSessionCreate(ticketContext).catch((err) => {
909
+ console.error('[index] transition on session create failed:', err);
910
+ });
911
+ }
912
+ res.status(201).json(session);
913
+ });
914
+ // POST /sessions/repo — start a session in the repo root (no worktree)
915
+ app.post('/sessions/repo', requireAuth, async (req, res) => {
916
+ const { repoPath, repoName, continue: continueSession, claudeArgs, yolo, agent, useTmux, cols, rows } = req.body;
917
+ if (!repoPath) {
918
+ res.status(400).json({ error: 'repoPath is required' });
919
+ return;
920
+ }
921
+ const resolved = resolveSessionSettings(config, repoPath, {
922
+ agent, yolo, continue: continueSession, useTmux, claudeArgs,
923
+ });
924
+ const resolvedAgent = resolved.agent;
925
+ // Sanitize optional terminal dimensions
926
+ const safeCols = typeof cols === 'number' && Number.isFinite(cols) && cols >= 1 && cols <= 500 ? Math.round(cols) : undefined;
927
+ const safeRows = typeof rows === 'number' && Number.isFinite(rows) && rows >= 1 && rows <= 200 ? Math.round(rows) : undefined;
928
+ // Multiple sessions per repo allowed (multi-tab support)
929
+ const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
930
+ const baseArgs = [
931
+ ...(resolved.claudeArgs),
932
+ ...(resolved.yolo ? AGENT_YOLO_ARGS[resolvedAgent] : []),
933
+ ];
934
+ const args = resolved.continue ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
935
+ const roots = config.rootDirs || [];
936
+ const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
937
+ let branchName = '';
938
+ try {
939
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoPath });
940
+ branchName = stdout.trim();
941
+ }
942
+ catch { /* non-fatal */ }
943
+ const session = sessions.create({
944
+ type: 'repo',
945
+ agent: resolvedAgent,
946
+ repoName: name,
947
+ repoPath,
948
+ cwd: repoPath,
949
+ root,
950
+ displayName: sessions.nextAgentName(),
951
+ args,
952
+ branchName,
953
+ useTmux: resolved.useTmux,
954
+ ...(safeCols != null && { cols: safeCols }),
955
+ ...(safeRows != null && { rows: safeRows }),
956
+ });
957
+ res.status(201).json(session);
958
+ });
959
+ // POST /sessions/terminal — start a bare shell session (no agent), optional cwd in body
960
+ app.post('/sessions/terminal', requireAuth, (req, res) => {
961
+ const shell = process.env.SHELL || '/bin/sh';
962
+ const displayName = sessions.nextTerminalName();
963
+ const rawCwd = req.body?.cwd;
964
+ const startDir = typeof rawCwd === 'string' && rawCwd.trim()
965
+ ? rawCwd.trim()
966
+ : os.homedir();
967
+ if (!fs.existsSync(startDir) || !fs.statSync(startDir).isDirectory()) {
968
+ res.status(400).json({ error: `Directory does not exist: ${startDir}` });
969
+ return;
970
+ }
971
+ const session = sessions.create({
972
+ type: 'terminal',
973
+ agent: 'claude', // required by CreateParams but unused for terminal sessions
974
+ repoPath: startDir,
975
+ cwd: startDir,
976
+ displayName,
977
+ command: shell,
978
+ args: [],
979
+ });
980
+ res.status(201).json(session);
981
+ });
982
+ // DELETE /sessions/:id
983
+ app.delete('/sessions/:id', requireAuth, (req, res) => {
984
+ const id = req.params['id'];
985
+ try {
986
+ sessions.kill(id);
987
+ push.removeSession(id);
988
+ res.json({ ok: true });
989
+ }
990
+ catch (_) {
991
+ res.status(404).json({ error: 'Session not found' });
992
+ }
993
+ });
994
+ // PATCH /sessions/:id — update displayName and persist to metadata
995
+ app.patch('/sessions/:id', requireAuth, (req, res) => {
996
+ const { displayName } = req.body;
997
+ if (!displayName) {
998
+ res.status(400).json({ error: 'displayName is required' });
999
+ return;
1000
+ }
1001
+ try {
1002
+ const id = req.params['id'];
1003
+ const updated = sessions.updateDisplayName(id, displayName);
1004
+ const session = sessions.get(id);
1005
+ if (session) {
1006
+ writeMeta(CONFIG_PATH, { worktreePath: session.repoPath, displayName, lastActivity: session.lastActivity });
1007
+ }
1008
+ res.json(updated);
1009
+ }
1010
+ catch (_) {
1011
+ res.status(404).json({ error: 'Session not found' });
1012
+ }
1013
+ });
1014
+ // POST /sessions/:id/image — upload clipboard image, proxy to system clipboard
1015
+ const ALLOWED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
1016
+ app.post('/sessions/:id/image', requireAuth, async (req, res) => {
1017
+ const { data, mimeType } = req.body;
1018
+ if (!data || !mimeType) {
1019
+ res.status(400).json({ error: 'data and mimeType are required' });
1020
+ return;
1021
+ }
1022
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType)) {
1023
+ res.status(400).json({ error: 'Unsupported image type: ' + mimeType });
1024
+ return;
1025
+ }
1026
+ // base64 is ~33% larger than binary; 10MB binary ≈ 13.3MB base64
1027
+ if (data.length > 14 * 1024 * 1024) {
1028
+ res.status(413).json({ error: 'Image too large (max 10MB)' });
1029
+ return;
1030
+ }
1031
+ const sessionId = req.params['id'];
1032
+ if (!sessions.get(sessionId)) {
1033
+ res.status(404).json({ error: 'Session not found' });
1034
+ return;
1035
+ }
1036
+ try {
1037
+ const ext = extensionForMime(mimeType);
1038
+ const dir = path.join(os.tmpdir(), 'claude-remote-cli', sessionId);
1039
+ fs.mkdirSync(dir, { recursive: true });
1040
+ const filePath = path.join(dir, 'paste-' + Date.now() + ext);
1041
+ fs.writeFileSync(filePath, Buffer.from(data, 'base64'));
1042
+ let clipboardSet = false;
1043
+ try {
1044
+ clipboardSet = await setClipboardImage(filePath, mimeType);
1045
+ }
1046
+ catch {
1047
+ // Clipboard tools failed — fall back to path
1048
+ }
1049
+ if (clipboardSet) {
1050
+ sessions.write(sessionId, '\x16');
1051
+ }
1052
+ res.json({ path: filePath, clipboardSet });
1053
+ }
1054
+ catch (err) {
1055
+ const message = err instanceof Error ? err.message : 'Image upload failed';
1056
+ res.status(500).json({ error: message });
1057
+ }
1058
+ });
1059
+ // GET /version — check current vs latest
1060
+ app.get('/version', requireAuth, async (_req, res) => {
1061
+ const current = getCurrentVersion();
1062
+ const latest = await getLatestVersion();
1063
+ const updateAvailable = latest !== null && semverLessThan(current, latest);
1064
+ res.json({ current, latest, updateAvailable });
1065
+ });
1066
+ // POST /update — install latest version from npm
1067
+ app.post('/update', requireAuth, async (_req, res) => {
1068
+ try {
1069
+ await execFileAsync('npm', ['install', '-g', 'claude-remote-cli@latest']);
1070
+ const restarting = serviceIsInstalled();
1071
+ if (restarting) {
1072
+ // Persist sessions so they can be restored after restart
1073
+ const configDir = path.dirname(CONFIG_PATH);
1074
+ serializeAll(configDir);
1075
+ }
1076
+ res.json({ ok: true, restarting });
1077
+ if (restarting) {
1078
+ setTimeout(() => process.exit(0), 1000);
1079
+ }
1080
+ }
1081
+ catch (err) {
1082
+ const message = err instanceof Error ? err.message : 'Update failed';
1083
+ res.status(500).json({ ok: false, error: message });
1084
+ }
1085
+ });
1086
+ // Clean up orphaned tmux sessions from previous runs (skip any adopted by restore)
1087
+ try {
1088
+ const adoptedNames = activeTmuxSessionNames();
1089
+ const { stdout } = await execFileAsync('tmux', ['list-sessions', '-F', '#{session_name}']);
1090
+ const orphanedSessions = stdout.trim().split('\n').filter(name => name.startsWith('crc-') && !adoptedNames.has(name));
1091
+ for (const name of orphanedSessions) {
1092
+ execFileAsync('tmux', ['kill-session', '-t', name]).catch(() => { });
1093
+ }
1094
+ if (orphanedSessions.length > 0) {
1095
+ console.log(`Cleaned up ${orphanedSessions.length} orphaned tmux session(s).`);
1096
+ }
1097
+ }
1098
+ catch {
1099
+ // tmux not installed or no sessions — ignore
1100
+ }
1101
+ function gracefulShutdown() {
1102
+ stopPolling();
1103
+ closeAnalytics();
1104
+ server.close();
1105
+ // Serialize sessions to disk BEFORE killing them
1106
+ const configDir = path.dirname(CONFIG_PATH);
1107
+ serializeAll(configDir);
1108
+ // Kill all active sessions (PTY + tmux)
1109
+ for (const s of sessions.list()) {
1110
+ try {
1111
+ sessions.kill(s.id);
1112
+ }
1113
+ catch { /* already exiting */ }
1114
+ }
1115
+ // Brief delay to let async tmux kill-session calls fire
1116
+ setTimeout(() => process.exit(0), 200);
1117
+ }
1118
+ process.on('SIGTERM', gracefulShutdown);
1119
+ process.on('SIGINT', gracefulShutdown);
1120
+ server.listen(config.port, config.host, () => {
1121
+ console.log(`claude-remote-cli listening on ${config.host}:${config.port}`);
1122
+ });
1123
+ }
1124
+ main().catch(console.error);