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,885 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import { Router } from 'express';
5
+ import { createLogger } from './logger.js';
6
+ let db = null;
7
+ let insertStmt = null;
8
+ const logger = createLogger('analytics');
9
+ let eventBuffer = [];
10
+ let batchTimer = null;
11
+ const BATCH_INTERVAL_MS = 500;
12
+ let insertSessionEventStmt = null;
13
+ let insertRateLimitStmt = null;
14
+ const SCHEMA_V1 = `
15
+ CREATE TABLE IF NOT EXISTS events (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
18
+ category TEXT NOT NULL, -- 'session', 'ui', 'agent', 'navigation', 'workspace'
19
+ action TEXT NOT NULL,
20
+ target TEXT,
21
+ properties TEXT,
22
+ session_id TEXT,
23
+ device TEXT
24
+ );
25
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
26
+ CREATE INDEX IF NOT EXISTS idx_events_category_action ON events(category, action);
27
+ CREATE INDEX IF NOT EXISTS idx_events_target ON events(target);
28
+ `;
29
+ const SCHEMA_V2 = `
30
+ CREATE TABLE IF NOT EXISTS session_events (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ session_id TEXT NOT NULL,
33
+ repo_path TEXT,
34
+ event_type TEXT NOT NULL,
35
+ event_data TEXT,
36
+ timestamp TEXT NOT NULL,
37
+ created_at TEXT DEFAULT (datetime('now'))
38
+ );
39
+ CREATE INDEX IF NOT EXISTS idx_sevents_session ON session_events(session_id);
40
+ CREATE INDEX IF NOT EXISTS idx_sevents_type ON session_events(event_type);
41
+ CREATE INDEX IF NOT EXISTS idx_sevents_timestamp ON session_events(timestamp);
42
+
43
+ CREATE TABLE IF NOT EXISTS session_rollups (
44
+ session_id TEXT PRIMARY KEY,
45
+ repo_path TEXT,
46
+ repo_name TEXT,
47
+ agent_type TEXT,
48
+ model TEXT,
49
+ started_at TEXT NOT NULL,
50
+ ended_at TEXT,
51
+ duration_seconds INTEGER,
52
+ total_input_tokens INTEGER DEFAULT 0,
53
+ total_output_tokens INTEGER DEFAULT 0,
54
+ total_cache_read INTEGER DEFAULT 0,
55
+ total_cache_write INTEGER DEFAULT 0,
56
+ turn_count INTEGER DEFAULT 0,
57
+ subagent_count INTEGER DEFAULT 0,
58
+ human_response_latency_avg_ms INTEGER,
59
+ human_response_latency_p50_ms INTEGER,
60
+ human_response_latency_p95_ms INTEGER,
61
+ agent_idle_percent REAL,
62
+ rate_limit_encounters INTEGER DEFAULT 0,
63
+ tool_use_counts TEXT,
64
+ recovered INTEGER DEFAULT 0,
65
+ updated_at TEXT DEFAULT (datetime('now'))
66
+ );
67
+ CREATE INDEX IF NOT EXISTS idx_rollups_repo ON session_rollups(repo_path);
68
+ CREATE INDEX IF NOT EXISTS idx_rollups_started ON session_rollups(started_at);
69
+
70
+ CREATE TABLE IF NOT EXISTS rate_limit_snapshots (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ five_hour_percent REAL,
73
+ five_hour_resets_at TEXT,
74
+ seven_day_percent REAL,
75
+ seven_day_resets_at TEXT,
76
+ timestamp TEXT NOT NULL,
77
+ created_at TEXT DEFAULT (datetime('now'))
78
+ );
79
+ CREATE INDEX IF NOT EXISTS idx_ratelimit_ts ON rate_limit_snapshots(timestamp);
80
+ `;
81
+ const MIGRATIONS = [
82
+ { version: 1, sql: SCHEMA_V1 },
83
+ { version: 2, sql: SCHEMA_V2 },
84
+ ];
85
+ const INSERT_SQL = 'INSERT INTO events (category, action, target, properties, session_id, device) VALUES (?, ?, ?, ?, ?, ?)';
86
+ export function initAnalytics(configDir) {
87
+ if (db) {
88
+ closeAnalytics();
89
+ }
90
+ const dbPath = path.join(configDir, 'analytics.db');
91
+ db = new Database(dbPath);
92
+ db.pragma('journal_mode = WAL');
93
+ // Schema version tracking
94
+ db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`);
95
+ const row = db.prepare('SELECT version FROM schema_version').get();
96
+ let currentVersion = row?.version ?? 0;
97
+ const hadRow = row !== undefined;
98
+ for (const migration of MIGRATIONS) {
99
+ if (migration.version > currentVersion) {
100
+ const ver = migration.version;
101
+ db.transaction(() => {
102
+ db.exec(migration.sql);
103
+ if (hadRow || currentVersion > 0) {
104
+ db.prepare('UPDATE schema_version SET version = ?').run(ver);
105
+ }
106
+ else {
107
+ db
108
+ .prepare('INSERT INTO schema_version (version) VALUES (?)')
109
+ .run(ver);
110
+ }
111
+ })();
112
+ currentVersion = ver;
113
+ }
114
+ }
115
+ insertStmt = db.prepare(INSERT_SQL);
116
+ }
117
+ function ensureSessionStmts() {
118
+ if (!db)
119
+ return;
120
+ if (!insertSessionEventStmt) {
121
+ insertSessionEventStmt = db.prepare('INSERT INTO session_events (session_id, repo_path, event_type, event_data, timestamp) VALUES (?, ?, ?, ?, ?)');
122
+ }
123
+ if (!insertRateLimitStmt) {
124
+ insertRateLimitStmt = db.prepare('INSERT INTO rate_limit_snapshots (five_hour_percent, five_hour_resets_at, seven_day_percent, seven_day_resets_at, timestamp) VALUES (?, ?, ?, ?, ?)');
125
+ }
126
+ }
127
+ export function recordSessionEvent(event) {
128
+ if (!db)
129
+ return;
130
+ eventBuffer.push(event);
131
+ }
132
+ export function flushEventBuffer(sessionId) {
133
+ if (!db || eventBuffer.length === 0)
134
+ return;
135
+ ensureSessionStmts();
136
+ if (!insertSessionEventStmt)
137
+ return;
138
+ const toFlush = sessionId
139
+ ? eventBuffer.filter((e) => e.session_id === sessionId)
140
+ : [...eventBuffer];
141
+ if (toFlush.length === 0)
142
+ return;
143
+ const stmt = insertSessionEventStmt;
144
+ const insertMany = db.transaction((events) => {
145
+ for (const e of events) {
146
+ stmt.run(e.session_id, e.repo_path ?? null, e.event_type, e.event_data ? JSON.stringify(e.event_data) : null, e.timestamp);
147
+ }
148
+ });
149
+ try {
150
+ insertMany(toFlush);
151
+ }
152
+ catch (err) {
153
+ logger.warn('Failed to flush event buffer:', err);
154
+ }
155
+ if (sessionId) {
156
+ eventBuffer = eventBuffer.filter((e) => e.session_id !== sessionId);
157
+ }
158
+ else {
159
+ eventBuffer = [];
160
+ }
161
+ }
162
+ export function recordRateLimitSnapshot(snapshot) {
163
+ if (!db)
164
+ return;
165
+ ensureSessionStmts();
166
+ if (!insertRateLimitStmt)
167
+ return;
168
+ try {
169
+ insertRateLimitStmt.run(snapshot.fiveHourPercent, snapshot.fiveHourResetsAt, snapshot.sevenDayPercent, snapshot.sevenDayResetsAt, snapshot.timestamp);
170
+ }
171
+ catch (err) {
172
+ logger.warn('Failed to record rate limit snapshot:', err);
173
+ }
174
+ }
175
+ export function startEventBatching() {
176
+ stopEventBatching();
177
+ batchTimer = setInterval(() => flushEventBuffer(), BATCH_INTERVAL_MS);
178
+ }
179
+ export function stopEventBatching() {
180
+ if (batchTimer) {
181
+ clearInterval(batchTimer);
182
+ batchTimer = null;
183
+ }
184
+ flushEventBuffer(); // flush remaining
185
+ }
186
+ export function closeAnalytics() {
187
+ stopEventBatching();
188
+ if (db) {
189
+ db.close();
190
+ db = null;
191
+ insertStmt = null;
192
+ insertSessionEventStmt = null;
193
+ insertRateLimitStmt = null;
194
+ }
195
+ }
196
+ function runInsert(stmt, event) {
197
+ stmt.run(event.category, event.action, event.target ?? null, event.properties ? JSON.stringify(event.properties) : null, event.session_id ?? null, event.device ?? null);
198
+ }
199
+ export function trackEvent(event) {
200
+ if (!insertStmt)
201
+ return;
202
+ try {
203
+ runInsert(insertStmt, event);
204
+ }
205
+ catch {
206
+ // Analytics write failure is non-fatal
207
+ }
208
+ }
209
+ export function getDbPath(configDir) {
210
+ return path.join(configDir, 'analytics.db');
211
+ }
212
+ export function getDbSize(configDir) {
213
+ try {
214
+ return fs.statSync(getDbPath(configDir)).size;
215
+ }
216
+ catch {
217
+ return 0;
218
+ }
219
+ }
220
+ export function createAnalyticsRouter(configDir) {
221
+ const router = Router();
222
+ // POST /analytics/events — batch ingest from frontend
223
+ router.post('/events', (req, res) => {
224
+ const { events } = req.body;
225
+ if (!Array.isArray(events)) {
226
+ res.status(400).json({ error: 'events array required' });
227
+ return;
228
+ }
229
+ if (!db || !insertStmt) {
230
+ res.status(503).json({ error: 'Analytics not initialized' });
231
+ return;
232
+ }
233
+ const stmt = insertStmt;
234
+ const insertMany = db.transaction((evts) => {
235
+ let inserted = 0;
236
+ for (const evt of evts) {
237
+ if (!evt.category || !evt.action)
238
+ continue;
239
+ runInsert(stmt, evt);
240
+ inserted++;
241
+ }
242
+ return inserted;
243
+ });
244
+ try {
245
+ const inserted = insertMany(events);
246
+ res.json({ ok: true, count: inserted });
247
+ }
248
+ catch {
249
+ res.status(500).json({ error: 'Failed to write events' });
250
+ }
251
+ });
252
+ // GET /analytics/size — DB file size in bytes
253
+ router.get('/size', (_req, res) => {
254
+ res.json({ bytes: getDbSize(configDir) });
255
+ });
256
+ // DELETE /analytics/events — truncate events table
257
+ router.delete('/events', (_req, res) => {
258
+ if (!db) {
259
+ res.status(503).json({ error: 'Analytics not initialized' });
260
+ return;
261
+ }
262
+ try {
263
+ db.exec('DELETE FROM events');
264
+ try {
265
+ db.pragma('wal_checkpoint(TRUNCATE)');
266
+ }
267
+ catch {
268
+ /* best-effort */
269
+ }
270
+ res.json({ ok: true });
271
+ }
272
+ catch {
273
+ res.status(500).json({ error: 'Failed to clear analytics' });
274
+ }
275
+ });
276
+ return router;
277
+ }
278
+ export function upsertSessionRollup(data) {
279
+ if (!db)
280
+ return;
281
+ const existing = db
282
+ .prepare('SELECT session_id FROM session_rollups WHERE session_id = ?')
283
+ .get(data.sessionId);
284
+ if (!existing) {
285
+ db.prepare(`
286
+ INSERT INTO session_rollups (session_id, repo_path, repo_name, agent_type, model, started_at, ended_at, duration_seconds,
287
+ total_input_tokens, total_output_tokens, total_cache_read, total_cache_write,
288
+ turn_count, subagent_count, human_response_latency_avg_ms, human_response_latency_p50_ms,
289
+ human_response_latency_p95_ms, agent_idle_percent, rate_limit_encounters, tool_use_counts, recovered, updated_at)
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
291
+ `).run(data.sessionId, data.repoPath ?? null, data.repoName ?? null, data.agentType ?? null, data.model ?? null, data.startedAt ?? new Date().toISOString(), data.endedAt ?? null, data.durationSeconds ?? null, data.totalInputTokens ?? 0, data.totalOutputTokens ?? 0, data.totalCacheRead ?? 0, data.totalCacheWrite ?? 0, data.turnCount ?? 0, data.subagentCount ?? 0, data.humanResponseLatencyAvgMs ?? null, data.humanResponseLatencyP50Ms ?? null, data.humanResponseLatencyP95Ms ?? null, data.agentIdlePercent ?? null, data.rateLimitEncounters ?? 0, data.toolUseCounts ? JSON.stringify(data.toolUseCounts) : null, data.recovered ? 1 : 0);
292
+ }
293
+ else {
294
+ const sets = ["updated_at = datetime('now')"];
295
+ const values = [];
296
+ const optionalFields = [
297
+ ['repoPath', 'repo_path'],
298
+ ['repoName', 'repo_name'],
299
+ ['agentType', 'agent_type'],
300
+ ['model', 'model'],
301
+ ['endedAt', 'ended_at'],
302
+ ['durationSeconds', 'duration_seconds'],
303
+ ['totalInputTokens', 'total_input_tokens'],
304
+ ['totalOutputTokens', 'total_output_tokens'],
305
+ ['totalCacheRead', 'total_cache_read'],
306
+ ['totalCacheWrite', 'total_cache_write'],
307
+ ['turnCount', 'turn_count'],
308
+ ['subagentCount', 'subagent_count'],
309
+ ['humanResponseLatencyAvgMs', 'human_response_latency_avg_ms'],
310
+ ['humanResponseLatencyP50Ms', 'human_response_latency_p50_ms'],
311
+ ['humanResponseLatencyP95Ms', 'human_response_latency_p95_ms'],
312
+ ['agentIdlePercent', 'agent_idle_percent'],
313
+ ['rateLimitEncounters', 'rate_limit_encounters'],
314
+ ];
315
+ for (const [key, col] of optionalFields) {
316
+ if (data[key] !== undefined) {
317
+ sets.push(`${col} = ?`);
318
+ values.push(data[key]);
319
+ }
320
+ }
321
+ if (data.recovered !== undefined) {
322
+ sets.push('recovered = ?');
323
+ values.push(data.recovered ? 1 : 0);
324
+ }
325
+ if (data.toolUseCounts !== undefined) {
326
+ sets.push('tool_use_counts = ?');
327
+ values.push(JSON.stringify(data.toolUseCounts));
328
+ }
329
+ values.push(data.sessionId);
330
+ db.prepare(`UPDATE session_rollups SET ${sets.join(', ')} WHERE session_id = ?`).run(...values);
331
+ }
332
+ }
333
+ export function getSessionRollup(sessionId) {
334
+ if (!db)
335
+ return null;
336
+ const row = db
337
+ .prepare('SELECT * FROM session_rollups WHERE session_id = ?')
338
+ .get(sessionId);
339
+ if (!row)
340
+ return null;
341
+ return {
342
+ sessionId: row.session_id,
343
+ repoPath: row.repo_path,
344
+ repoName: row.repo_name,
345
+ agentType: row.agent_type,
346
+ model: row.model,
347
+ startedAt: row.started_at,
348
+ endedAt: row.ended_at,
349
+ durationSeconds: row.duration_seconds,
350
+ totalInputTokens: row.total_input_tokens,
351
+ totalOutputTokens: row.total_output_tokens,
352
+ totalCacheRead: row.total_cache_read,
353
+ totalCacheWrite: row.total_cache_write,
354
+ turnCount: row.turn_count,
355
+ subagentCount: row.subagent_count,
356
+ humanResponseLatencyAvgMs: row.human_response_latency_avg_ms,
357
+ humanResponseLatencyP50Ms: row.human_response_latency_p50_ms,
358
+ humanResponseLatencyP95Ms: row.human_response_latency_p95_ms,
359
+ agentIdlePercent: row.agent_idle_percent,
360
+ rateLimitEncounters: row.rate_limit_encounters,
361
+ toolUseCounts: row.tool_use_counts
362
+ ? JSON.parse(row.tool_use_counts)
363
+ : null,
364
+ recovered: row.recovered === 1,
365
+ };
366
+ }
367
+ function percentile(sorted, p) {
368
+ if (sorted.length === 0)
369
+ return 0;
370
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
371
+ return sorted[Math.max(0, idx)];
372
+ }
373
+ export function computeEngagementMetrics(sessionId) {
374
+ if (!db)
375
+ return null;
376
+ const events = db
377
+ .prepare('SELECT event_type, event_data, timestamp FROM session_events WHERE session_id = ? ORDER BY timestamp ASC')
378
+ .all(sessionId);
379
+ if (events.length === 0)
380
+ return null;
381
+ // 1. Human response latency: last notification before each user_prompt
382
+ const latencySamples = [];
383
+ let lastNotificationTime = null;
384
+ let inIdlePeriod = false;
385
+ for (const e of events) {
386
+ const ts = new Date(e.timestamp).getTime();
387
+ if (e.event_type === 'agent_stop') {
388
+ inIdlePeriod = true;
389
+ lastNotificationTime = null;
390
+ }
391
+ else if (e.event_type === 'notification' && inIdlePeriod) {
392
+ lastNotificationTime = ts;
393
+ }
394
+ else if (e.event_type === 'user_prompt') {
395
+ if (lastNotificationTime !== null) {
396
+ latencySamples.push(ts - lastNotificationTime);
397
+ }
398
+ inIdlePeriod = false;
399
+ lastNotificationTime = null;
400
+ }
401
+ }
402
+ const sortedLatency = [...latencySamples].sort((a, b) => a - b);
403
+ const avgLatency = sortedLatency.length > 0
404
+ ? Math.round(sortedLatency.reduce((a, b) => a + b, 0) / sortedLatency.length)
405
+ : null;
406
+ // 2. Agent idle %
407
+ const firstTs = new Date(events[0].timestamp).getTime();
408
+ const lastTs = new Date(events[events.length - 1].timestamp).getTime();
409
+ const totalDuration = lastTs - firstTs;
410
+ const stopFailureTimes = events
411
+ .filter((e) => e.event_type === 'stop_failure')
412
+ .map((e) => ({
413
+ timestamp: new Date(e.timestamp).getTime(),
414
+ isRateLimit: (() => {
415
+ try {
416
+ const data = e.event_data
417
+ ? JSON.parse(e.event_data)
418
+ : {};
419
+ return data.error === 'rate_limit';
420
+ }
421
+ catch {
422
+ return false;
423
+ }
424
+ })(),
425
+ }));
426
+ let waitingForHumanMs = 0;
427
+ let lastActiveStart = firstTs;
428
+ for (let i = 0; i < events.length; i++) {
429
+ const e = events[i];
430
+ const ts = new Date(e.timestamp).getTime();
431
+ if (e.event_type === 'agent_stop') {
432
+ lastActiveStart = null;
433
+ }
434
+ else if (e.event_type === 'user_prompt') {
435
+ if (lastActiveStart === null) {
436
+ let idleStart = null;
437
+ for (let j = i - 1; j >= 0; j--) {
438
+ if (events[j].event_type === 'agent_stop') {
439
+ idleStart = new Date(events[j].timestamp).getTime();
440
+ break;
441
+ }
442
+ }
443
+ if (idleStart !== null) {
444
+ const idleMs = ts - idleStart;
445
+ const hasRateLimit = stopFailureTimes.some((sf) => sf.isRateLimit && sf.timestamp >= idleStart && sf.timestamp <= ts);
446
+ if (!hasRateLimit) {
447
+ waitingForHumanMs += idleMs;
448
+ }
449
+ }
450
+ }
451
+ lastActiveStart = ts;
452
+ }
453
+ }
454
+ const agentIdlePercent = totalDuration > 0
455
+ ? Math.round((waitingForHumanMs / totalDuration) * 1000) / 10
456
+ : null;
457
+ // 3. Rate limit encounters
458
+ const rateLimitEncounters = stopFailureTimes.filter((sf) => sf.isRateLimit).length;
459
+ // 4. Tool use counts
460
+ const toolUseCounts = {};
461
+ for (const e of events) {
462
+ if (e.event_type === 'tool_use' && e.event_data) {
463
+ try {
464
+ const data = JSON.parse(e.event_data);
465
+ const tool = typeof data.tool === 'string' ? data.tool : 'unknown';
466
+ toolUseCounts[tool] = (toolUseCounts[tool] ?? 0) + 1;
467
+ }
468
+ catch {
469
+ /* malformed */
470
+ }
471
+ }
472
+ }
473
+ return {
474
+ humanResponseLatencyAvgMs: avgLatency,
475
+ humanResponseLatencyP50Ms: sortedLatency.length > 0 ? percentile(sortedLatency, 50) : null,
476
+ humanResponseLatencyP95Ms: sortedLatency.length > 0 ? percentile(sortedLatency, 95) : null,
477
+ agentIdlePercent,
478
+ rateLimitEncounters,
479
+ toolUseCounts,
480
+ };
481
+ }
482
+ // ── Retention Policy ──
483
+ export function runRetentionCleanup(retentionDays = 90) {
484
+ if (!db)
485
+ return;
486
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
487
+ // Delete old session events
488
+ db.prepare('DELETE FROM session_events WHERE timestamp < ?').run(cutoff);
489
+ // Downsample rate_limit_snapshots: after 7 days keep hourly, after 30 days keep daily
490
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
491
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
492
+ // Keep first snapshot per hour for entries older than 7 days but newer than 30 days
493
+ db.prepare(`
494
+ DELETE FROM rate_limit_snapshots
495
+ WHERE timestamp < ? AND timestamp >= ?
496
+ AND id NOT IN (
497
+ SELECT MIN(id) FROM rate_limit_snapshots
498
+ WHERE timestamp < ? AND timestamp >= ?
499
+ GROUP BY strftime('%Y-%m-%d %H', timestamp)
500
+ )
501
+ `).run(sevenDaysAgo, thirtyDaysAgo, sevenDaysAgo, thirtyDaysAgo);
502
+ // Keep first snapshot per day for entries older than 30 days
503
+ db.prepare(`
504
+ DELETE FROM rate_limit_snapshots
505
+ WHERE timestamp < ?
506
+ AND id NOT IN (
507
+ SELECT MIN(id) FROM rate_limit_snapshots
508
+ WHERE timestamp < ?
509
+ GROUP BY strftime('%Y-%m-%d', timestamp)
510
+ )
511
+ `).run(thirtyDaysAgo, thirtyDaysAgo);
512
+ }
513
+ // ── Orphaned Session Recovery ──
514
+ export function recoverOrphanedSessions() {
515
+ if (!db)
516
+ return 0;
517
+ // Normalize the threshold and stored updated_at to a common format for comparison.
518
+ // SQLite's datetime('now') returns 'YYYY-MM-DD HH:MM:SS'; JS toISOString() returns
519
+ // 'YYYY-MM-DDTHH:MM:SS.mmmZ'. We use datetime() on both sides to normalize.
520
+ const staleThreshold = new Date(Date.now() - 10 * 60 * 1000).toISOString();
521
+ const orphans = db
522
+ .prepare(`
523
+ SELECT session_id FROM session_rollups
524
+ WHERE ended_at IS NULL AND datetime(updated_at) < datetime(?)
525
+ `)
526
+ .all(staleThreshold);
527
+ for (const { session_id } of orphans) {
528
+ const lastEvent = db
529
+ .prepare('SELECT timestamp FROM session_events WHERE session_id = ? ORDER BY timestamp DESC LIMIT 1')
530
+ .get(session_id);
531
+ const endedAt = lastEvent?.timestamp ?? new Date().toISOString();
532
+ const metrics = computeEngagementMetrics(session_id);
533
+ if (metrics) {
534
+ upsertSessionRollup({
535
+ sessionId: session_id,
536
+ endedAt,
537
+ recovered: true,
538
+ ...(metrics.humanResponseLatencyAvgMs !== null
539
+ ? { humanResponseLatencyAvgMs: metrics.humanResponseLatencyAvgMs }
540
+ : {}),
541
+ ...(metrics.humanResponseLatencyP50Ms !== null
542
+ ? { humanResponseLatencyP50Ms: metrics.humanResponseLatencyP50Ms }
543
+ : {}),
544
+ ...(metrics.humanResponseLatencyP95Ms !== null
545
+ ? { humanResponseLatencyP95Ms: metrics.humanResponseLatencyP95Ms }
546
+ : {}),
547
+ ...(metrics.agentIdlePercent !== null
548
+ ? { agentIdlePercent: metrics.agentIdlePercent }
549
+ : {}),
550
+ rateLimitEncounters: metrics.rateLimitEncounters,
551
+ toolUseCounts: metrics.toolUseCounts,
552
+ });
553
+ }
554
+ else {
555
+ upsertSessionRollup({
556
+ sessionId: session_id,
557
+ endedAt,
558
+ recovered: true,
559
+ });
560
+ }
561
+ }
562
+ return orphans.length;
563
+ }
564
+ // ── Session Analytics REST API ──
565
+ export function createSessionAnalyticsRouter() {
566
+ const router = Router();
567
+ // GET /overview
568
+ router.get('/overview', (_req, res) => {
569
+ if (!db) {
570
+ res.status(503).json({ error: 'Analytics not initialized' });
571
+ return;
572
+ }
573
+ const days = parseInt(_req.query.days) || 7;
574
+ const repoFilter = _req.query.repo;
575
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
576
+ let query = `SELECT * FROM session_rollups WHERE started_at >= ?`;
577
+ const params = [since];
578
+ if (repoFilter) {
579
+ query += ' AND repo_path = ?';
580
+ params.push(repoFilter);
581
+ }
582
+ const rollups = db.prepare(query).all(...params);
583
+ let totalTokensIn = 0, totalTokensOut = 0, totalCacheRead = 0;
584
+ let totalDuration = 0, durationCount = 0;
585
+ let totalLatency = 0, latencyCount = 0;
586
+ let totalIdle = 0, idleCount = 0;
587
+ let totalRateLimits = 0;
588
+ const byRepo = new Map();
589
+ for (const r of rollups) {
590
+ totalTokensIn += r.total_input_tokens;
591
+ totalTokensOut += r.total_output_tokens;
592
+ totalCacheRead += r.total_cache_read;
593
+ if (r.duration_seconds != null) {
594
+ totalDuration += r.duration_seconds;
595
+ durationCount++;
596
+ }
597
+ if (r.human_response_latency_avg_ms != null) {
598
+ totalLatency += r.human_response_latency_avg_ms;
599
+ latencyCount++;
600
+ }
601
+ if (r.agent_idle_percent != null) {
602
+ totalIdle += r.agent_idle_percent;
603
+ idleCount++;
604
+ }
605
+ totalRateLimits += r.rate_limit_encounters;
606
+ const rp = r.repo_path ?? 'unknown';
607
+ const existing = byRepo.get(rp);
608
+ if (existing) {
609
+ existing.sessions++;
610
+ existing.tokensIn += r.total_input_tokens;
611
+ existing.tokensOut += r.total_output_tokens;
612
+ }
613
+ else {
614
+ byRepo.set(rp, {
615
+ repoName: r.repo_name ?? rp.split('/').pop() ?? rp,
616
+ sessions: 1,
617
+ tokensIn: r.total_input_tokens,
618
+ tokensOut: r.total_output_tokens,
619
+ });
620
+ }
621
+ }
622
+ const totalTokens = totalTokensIn + totalTokensOut;
623
+ const byRepoArr = [...byRepo.entries()]
624
+ .map(([_, v]) => ({
625
+ ...v,
626
+ pctOfTotal: totalTokens > 0
627
+ ? Math.round(((v.tokensIn + v.tokensOut) / totalTokens) * 1000) / 10
628
+ : 0,
629
+ }))
630
+ .sort((a, b) => b.tokensIn - a.tokensIn);
631
+ res.json({
632
+ timeWindow: { start: since, end: new Date().toISOString() },
633
+ totalSessions: rollups.length,
634
+ totalTokensIn,
635
+ totalTokensOut,
636
+ totalCacheRead,
637
+ avgSessionDuration: durationCount > 0 ? Math.round(totalDuration / durationCount) : 0,
638
+ avgHumanResponseLatency: latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0,
639
+ avgAgentIdlePercent: idleCount > 0 ? Math.round((totalIdle / idleCount) * 10) / 10 : 0,
640
+ totalRateLimitEncounters: totalRateLimits,
641
+ byRepo: byRepoArr,
642
+ });
643
+ });
644
+ // GET /sessions
645
+ router.get('/sessions', (_req, res) => {
646
+ if (!db) {
647
+ res.status(503).json({ error: 'Analytics not initialized' });
648
+ return;
649
+ }
650
+ const offset = Math.max(parseInt(_req.query.offset) || 0, 0);
651
+ const limit = Math.min(Math.max(parseInt(_req.query.limit) || 20, 1), 100);
652
+ const repoFilter = _req.query.repo;
653
+ const agentFilter = _req.query.agent;
654
+ const sort = _req.query.sort || 'started_at';
655
+ const validSorts = {
656
+ started_at: 'started_at DESC',
657
+ tokens: 'total_input_tokens DESC',
658
+ duration: 'duration_seconds DESC',
659
+ };
660
+ const orderBy = validSorts[sort] ?? 'started_at DESC';
661
+ let where = 'WHERE 1=1';
662
+ const params = [];
663
+ if (repoFilter) {
664
+ where += ' AND repo_path = ?';
665
+ params.push(repoFilter);
666
+ }
667
+ if (agentFilter) {
668
+ where += ' AND agent_type = ?';
669
+ params.push(agentFilter);
670
+ }
671
+ const total = db
672
+ .prepare(`SELECT COUNT(*) as count FROM session_rollups ${where}`)
673
+ .get(...params).count;
674
+ const rows = db
675
+ .prepare(`SELECT * FROM session_rollups ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`)
676
+ .all(...params, limit, offset);
677
+ const sessions = rows.map((r) => ({
678
+ sessionId: r.session_id,
679
+ repoName: r.repo_name,
680
+ repoPath: r.repo_path,
681
+ agentType: r.agent_type,
682
+ model: r.model,
683
+ startedAt: r.started_at,
684
+ endedAt: r.ended_at,
685
+ durationSeconds: r.duration_seconds,
686
+ totalInputTokens: r.total_input_tokens,
687
+ totalOutputTokens: r.total_output_tokens,
688
+ turnCount: r.turn_count,
689
+ humanResponseLatencyAvg: r.human_response_latency_avg_ms,
690
+ agentIdlePercent: r.agent_idle_percent,
691
+ rateLimitEncounters: r.rate_limit_encounters,
692
+ topTools: (() => {
693
+ try {
694
+ const counts = r.tool_use_counts
695
+ ? JSON.parse(r.tool_use_counts)
696
+ : {};
697
+ return Object.entries(counts)
698
+ .sort((a, b) => b[1] - a[1])
699
+ .slice(0, 3)
700
+ .map(([name]) => name);
701
+ }
702
+ catch {
703
+ return [];
704
+ }
705
+ })(),
706
+ recovered: r.recovered === 1,
707
+ }));
708
+ res.json({ sessions, total, offset, limit });
709
+ });
710
+ // GET /sessions/:id
711
+ router.get('/sessions/:id', (req, res) => {
712
+ if (!db) {
713
+ res.status(503).json({ error: 'Analytics not initialized' });
714
+ return;
715
+ }
716
+ const sessionId = req.params['id'];
717
+ if (!sessionId) {
718
+ res.status(400).json({ error: 'Missing session ID' });
719
+ return;
720
+ }
721
+ const rollup = getSessionRollup(sessionId);
722
+ if (!rollup) {
723
+ res.status(404).json({ error: 'Session not found' });
724
+ return;
725
+ }
726
+ const events = db
727
+ .prepare('SELECT event_type, event_data, timestamp FROM session_events WHERE session_id = ? ORDER BY timestamp ASC')
728
+ .all(sessionId);
729
+ const toolBreakdown = {};
730
+ if (rollup.toolUseCounts) {
731
+ for (const [tool, count] of Object.entries(rollup.toolUseCounts)) {
732
+ toolBreakdown[tool] = { count };
733
+ }
734
+ }
735
+ let agentActiveTime = 0, waitingForHumanTime = 0, rateLimitTime = 0;
736
+ if (events.length >= 2) {
737
+ const firstTs = new Date(events[0].timestamp).getTime();
738
+ const lastTs = new Date(events[events.length - 1].timestamp).getTime();
739
+ const totalMs = lastTs - firstTs;
740
+ if (rollup.agentIdlePercent !== null && totalMs > 0) {
741
+ waitingForHumanTime = Math.round((totalMs * (rollup.agentIdlePercent / 100)) / 1000);
742
+ agentActiveTime = Math.round((totalMs - waitingForHumanTime * 1000) / 1000);
743
+ }
744
+ else {
745
+ agentActiveTime = Math.round(totalMs / 1000);
746
+ }
747
+ }
748
+ res.json({
749
+ session: rollup,
750
+ toolBreakdown,
751
+ events: events.map((e) => ({
752
+ type: e.event_type,
753
+ timestamp: e.timestamp,
754
+ data: e.event_data
755
+ ? (() => {
756
+ try {
757
+ return JSON.parse(e.event_data);
758
+ }
759
+ catch {
760
+ return {};
761
+ }
762
+ })()
763
+ : {},
764
+ })),
765
+ engagementBreakdown: {
766
+ agentActiveTime,
767
+ waitingForHumanTime,
768
+ rateLimitTime,
769
+ otherTime: Math.max(0, (rollup.durationSeconds ?? 0) -
770
+ agentActiveTime -
771
+ waitingForHumanTime -
772
+ rateLimitTime),
773
+ },
774
+ });
775
+ });
776
+ // GET /trends
777
+ router.get('/trends', (_req, res) => {
778
+ if (!db) {
779
+ res.status(503).json({ error: 'Analytics not initialized' });
780
+ return;
781
+ }
782
+ const days = parseInt(_req.query.days) || 30;
783
+ const repoFilter = _req.query.repo;
784
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
785
+ let query = `
786
+ SELECT
787
+ date(started_at) as date,
788
+ COUNT(*) as sessions,
789
+ SUM(total_input_tokens) as tokens_in,
790
+ SUM(total_output_tokens) as tokens_out,
791
+ AVG(human_response_latency_avg_ms) as avg_human_latency,
792
+ AVG(agent_idle_percent) as avg_agent_idle,
793
+ SUM(rate_limit_encounters) as rate_limit_encounters
794
+ FROM session_rollups
795
+ WHERE started_at >= ?
796
+ `;
797
+ const params = [since];
798
+ if (repoFilter) {
799
+ query += ' AND repo_path = ?';
800
+ params.push(repoFilter);
801
+ }
802
+ query += ' GROUP BY date(started_at) ORDER BY date ASC';
803
+ const rows = db.prepare(query).all(...params);
804
+ res.json({
805
+ days: rows.map((r) => ({
806
+ date: r.date,
807
+ sessions: r.sessions,
808
+ tokensIn: r.tokens_in ?? 0,
809
+ tokensOut: r.tokens_out ?? 0,
810
+ avgHumanLatency: r.avg_human_latency
811
+ ? Math.round(r.avg_human_latency)
812
+ : 0,
813
+ avgAgentIdle: r.avg_agent_idle
814
+ ? Math.round(r.avg_agent_idle * 10) / 10
815
+ : 0,
816
+ rateLimitEncounters: r.rate_limit_encounters ?? 0,
817
+ })),
818
+ });
819
+ });
820
+ // GET /tools
821
+ router.get('/tools', (_req, res) => {
822
+ if (!db) {
823
+ res.status(503).json({ error: 'Analytics not initialized' });
824
+ return;
825
+ }
826
+ const days = parseInt(_req.query.days) || 7;
827
+ const repoFilter = _req.query.repo;
828
+ const sessionFilter = _req.query.session;
829
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
830
+ let query = `SELECT event_data FROM session_events WHERE event_type = 'tool_use' AND timestamp >= ?`;
831
+ const params = [since];
832
+ if (repoFilter) {
833
+ query += ' AND repo_path = ?';
834
+ params.push(repoFilter);
835
+ }
836
+ if (sessionFilter) {
837
+ query += ' AND session_id = ?';
838
+ params.push(sessionFilter);
839
+ }
840
+ const rows = db.prepare(query).all(...params);
841
+ const counts = new Map();
842
+ let totalUses = 0;
843
+ for (const r of rows) {
844
+ if (!r.event_data)
845
+ continue;
846
+ try {
847
+ const data = JSON.parse(r.event_data);
848
+ const tool = typeof data.tool === 'string' ? data.tool : 'unknown';
849
+ counts.set(tool, (counts.get(tool) ?? 0) + 1);
850
+ totalUses++;
851
+ }
852
+ catch {
853
+ /* ignore */
854
+ }
855
+ }
856
+ const tools = [...counts.entries()]
857
+ .map(([name, count]) => ({
858
+ name,
859
+ totalUses: count,
860
+ pctOfUses: totalUses > 0 ? Math.round((count / totalUses) * 1000) / 10 : 0,
861
+ }))
862
+ .sort((a, b) => b.totalUses - a.totalUses);
863
+ res.json({ tools });
864
+ });
865
+ // GET /rate-limits
866
+ router.get('/rate-limits', (_req, res) => {
867
+ if (!db) {
868
+ res.status(503).json({ error: 'Analytics not initialized' });
869
+ return;
870
+ }
871
+ const hours = parseInt(_req.query.hours) || 24;
872
+ const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
873
+ const rows = db
874
+ .prepare('SELECT * FROM rate_limit_snapshots WHERE timestamp >= ? ORDER BY timestamp ASC')
875
+ .all(since);
876
+ res.json({
877
+ snapshots: rows.map((r) => ({
878
+ timestamp: r.timestamp,
879
+ fiveHourPercent: r.five_hour_percent,
880
+ sevenDayPercent: r.seven_day_percent,
881
+ })),
882
+ });
883
+ });
884
+ return router;
885
+ }