specrails-desktop 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/.claude/commands/specrails/batch-implement.md +287 -0
  2. package/.claude/commands/specrails/compat-check.md +271 -0
  3. package/.claude/commands/specrails/doctor.md +62 -0
  4. package/.claude/commands/specrails/enrich.md +1635 -0
  5. package/.claude/commands/specrails/explore-spec.md +173 -0
  6. package/.claude/commands/specrails/health-check.md +527 -0
  7. package/.claude/commands/specrails/implement.md +1457 -0
  8. package/.claude/commands/specrails/memory-inspect.md +259 -0
  9. package/.claude/commands/specrails/opsx-diff.md +419 -0
  10. package/.claude/commands/specrails/propose-spec.md +102 -0
  11. package/.claude/commands/specrails/reconfig.md +89 -0
  12. package/.claude/commands/specrails/refactor-recommender.md +212 -0
  13. package/.claude/commands/specrails/retry.md +363 -0
  14. package/.claude/commands/specrails/telemetry.md +552 -0
  15. package/.claude/commands/specrails/why.md +96 -0
  16. package/LICENSE +21 -0
  17. package/README.md +290 -0
  18. package/cli/dist/specrails-desktop.js +1098 -0
  19. package/client/dist/assets/ActivityFeedPage-Gy4x8dBt.js +1 -0
  20. package/client/dist/assets/AgentsPage-CPgu--Fb.js +86 -0
  21. package/client/dist/assets/AnalyticsPage-B5sJEee2.js +1 -0
  22. package/client/dist/assets/BarChart-7IMQ8HY1.js +33 -0
  23. package/client/dist/assets/CodePage-CBdFvbwe.js +2 -0
  24. package/client/dist/assets/DesktopAnalyticsPage-w0rdTq4w.js +1 -0
  25. package/client/dist/assets/DocsDialog-BZUYM7wm.js +11 -0
  26. package/client/dist/assets/DocsPage-9QglWl46.js +11 -0
  27. package/client/dist/assets/ExportDropdown-BLZFXtNi.js +1 -0
  28. package/client/dist/assets/IntegrationsPage-BxBE4y99.js +3 -0
  29. package/client/dist/assets/JobDetailPage-DydWx_5S.js +16 -0
  30. package/client/dist/assets/JobsPage-20ibw0IO.js +1 -0
  31. package/client/dist/assets/abap-Bw6f2wDG.js +1 -0
  32. package/client/dist/assets/activity-BEIp_Y1A.js +1 -0
  33. package/client/dist/assets/activity-BdrPln96.js +1 -0
  34. package/client/dist/assets/activity-CpkRS8Sx.js +1 -0
  35. package/client/dist/assets/activity-DKCpESPt.js +1 -0
  36. package/client/dist/assets/activity-DOUVEjJi.js +1 -0
  37. package/client/dist/assets/activity-DRwkql_y.js +1 -0
  38. package/client/dist/assets/activity-DcDQ7tjw.js +1 -0
  39. package/client/dist/assets/activity-Dv6H7wEr.js +1 -0
  40. package/client/dist/assets/addon-image-3WCl5Vhd.js +1 -0
  41. package/client/dist/assets/addon-ligatures-C5OdliKs.js +2 -0
  42. package/client/dist/assets/addon-webgl-BbX6pSjl.js +44 -0
  43. package/client/dist/assets/addspec-B5yl4Loj.js +1 -0
  44. package/client/dist/assets/addspec-BEeF5-zc.js +1 -0
  45. package/client/dist/assets/addspec-D33ocMxf.js +1 -0
  46. package/client/dist/assets/addspec-DFswZ0jK.js +1 -0
  47. package/client/dist/assets/addspec-DRE-jZv7.js +1 -0
  48. package/client/dist/assets/addspec-DVZ15Jp8.js +1 -0
  49. package/client/dist/assets/addspec-Fkv91Opc.js +1 -0
  50. package/client/dist/assets/addspec-GWm4ffKl.js +1 -0
  51. package/client/dist/assets/agents-1nCDWRmP.js +1 -0
  52. package/client/dist/assets/agents-Bm9rPqnt.js +1 -0
  53. package/client/dist/assets/agents-CMxtJMLD.js +1 -0
  54. package/client/dist/assets/agents-DK-Dlc0i.js +1 -0
  55. package/client/dist/assets/agents-Q6Ldfpxx.js +1 -0
  56. package/client/dist/assets/agents-TeOSy-ax.js +1 -0
  57. package/client/dist/assets/agents-iTqjRajS.js +1 -0
  58. package/client/dist/assets/agents-s87sMGzL.js +1 -0
  59. package/client/dist/assets/agentstudio-B6Wb59E7.js +1 -0
  60. package/client/dist/assets/agentstudio-BADhZ41e.js +1 -0
  61. package/client/dist/assets/agentstudio-BSnWLR63.js +1 -0
  62. package/client/dist/assets/agentstudio-BdidyBzZ.js +1 -0
  63. package/client/dist/assets/agentstudio-CxlUllqI.js +1 -0
  64. package/client/dist/assets/agentstudio-D3I62TLJ.js +1 -0
  65. package/client/dist/assets/agentstudio-DuH9TogZ.js +1 -0
  66. package/client/dist/assets/agentstudio-Kw88_dUF.js +1 -0
  67. package/client/dist/assets/aiedit-BWxHGsYA.js +1 -0
  68. package/client/dist/assets/aiedit-D2ji6Qy0.js +1 -0
  69. package/client/dist/assets/aiedit-DAhZTvtk.js +1 -0
  70. package/client/dist/assets/aiedit-DJMny-D5.js +1 -0
  71. package/client/dist/assets/aiedit-DOcxERkU.js +1 -0
  72. package/client/dist/assets/aiedit-DvrcbwGv.js +1 -0
  73. package/client/dist/assets/aiedit-TTwzL1TS.js +1 -0
  74. package/client/dist/assets/aiedit-WBSjT_C1.js +1 -0
  75. package/client/dist/assets/analytics-BIdr0YfL.js +1 -0
  76. package/client/dist/assets/analytics-C6EzgtdE.js +1 -0
  77. package/client/dist/assets/analytics-C9Zc-rkM.js +1 -0
  78. package/client/dist/assets/analytics-CVx3YOc0.js +1 -0
  79. package/client/dist/assets/analytics-CYj0tfj7.js +1 -0
  80. package/client/dist/assets/analytics-CnY4kNG3.js +1 -0
  81. package/client/dist/assets/analytics-CrPCZRJ-.js +1 -0
  82. package/client/dist/assets/analytics-DMCto-TF.js +1 -0
  83. package/client/dist/assets/apex-Cw8_REBo.js +1 -0
  84. package/client/dist/assets/atom-one-dark-B-oHczHB.css +1 -0
  85. package/client/dist/assets/attachments-BIsSSnHJ.js +1 -0
  86. package/client/dist/assets/attachments-BW4L3l2L.js +1 -0
  87. package/client/dist/assets/attachments-Bcf6BG6V.js +1 -0
  88. package/client/dist/assets/attachments-Bke8sCU4.js +1 -0
  89. package/client/dist/assets/attachments-COcrGRFz.js +1 -0
  90. package/client/dist/assets/attachments-DYHGA2Dj.js +1 -0
  91. package/client/dist/assets/attachments-Dd92KpUH.js +1 -0
  92. package/client/dist/assets/attachments-DzdU6DV6.js +1 -0
  93. package/client/dist/assets/azcli-Cz6HAoOw.js +1 -0
  94. package/client/dist/assets/bat-CcJ-xyqL.js +1 -0
  95. package/client/dist/assets/bicep-z1WDCKYz.js +2 -0
  96. package/client/dist/assets/browser-5ErDlJoR.js +1 -0
  97. package/client/dist/assets/browser-Bc-YdlVg.js +1 -0
  98. package/client/dist/assets/browser-BlYF4OOq.js +1 -0
  99. package/client/dist/assets/browser-CT-ReZGt.js +1 -0
  100. package/client/dist/assets/browser-DGITz3fC.js +1 -0
  101. package/client/dist/assets/browser-JsAIGCEW.js +1 -0
  102. package/client/dist/assets/browser-M5-rbPlw.js +1 -0
  103. package/client/dist/assets/browser-Qya9cARy.js +1 -0
  104. package/client/dist/assets/cameligo-BRewOpfa.js +1 -0
  105. package/client/dist/assets/chat-BEGuC03z.js +1 -0
  106. package/client/dist/assets/chat-BEW60P_u.js +1 -0
  107. package/client/dist/assets/chat-BQNMD0PL.js +1 -0
  108. package/client/dist/assets/chat-BsbNGPW9.js +1 -0
  109. package/client/dist/assets/chat-CboQguCi.js +1 -0
  110. package/client/dist/assets/chat-DRCa9pOt.js +1 -0
  111. package/client/dist/assets/chat-DwUm6W9z.js +1 -0
  112. package/client/dist/assets/chat-yoXwguQu.js +1 -0
  113. package/client/dist/assets/chunk-CilyBKbf.js +1 -0
  114. package/client/dist/assets/clojure-DBjRWN6g.js +1 -0
  115. package/client/dist/assets/clsx-DnqN-uhr.js +1 -0
  116. package/client/dist/assets/code-AL1rVIMb.js +1 -0
  117. package/client/dist/assets/code-C0BKpkht.js +1 -0
  118. package/client/dist/assets/code-C0FTS3ew.js +1 -0
  119. package/client/dist/assets/code-CPcHxzxw.js +1 -0
  120. package/client/dist/assets/code-D3ryDniw.js +1 -0
  121. package/client/dist/assets/code-D3zVVQTj.js +1 -0
  122. package/client/dist/assets/code-PCmfS3dn.js +1 -0
  123. package/client/dist/assets/code-exI0G5Wd.js +1 -0
  124. package/client/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  125. package/client/dist/assets/coffee-Cfk_XHGR.js +1 -0
  126. package/client/dist/assets/commands-B772IyDa.js +1 -0
  127. package/client/dist/assets/commands-BDDp6xFG.js +1 -0
  128. package/client/dist/assets/commands-CJxCry-o.js +1 -0
  129. package/client/dist/assets/commands-CfgY-_of.js +1 -0
  130. package/client/dist/assets/commands-DLrvnPNg.js +1 -0
  131. package/client/dist/assets/commands-IXMOKBYt.js +1 -0
  132. package/client/dist/assets/commands-UD1NzmwX.js +1 -0
  133. package/client/dist/assets/commands-sqrqsxyE.js +1 -0
  134. package/client/dist/assets/common-DCr6VzJ7.js +1 -0
  135. package/client/dist/assets/common-Dard9UNH.js +1 -0
  136. package/client/dist/assets/common-DeDELLZJ.js +1 -0
  137. package/client/dist/assets/common-DltqHaAe.js +1 -0
  138. package/client/dist/assets/common-Dmm1GhdD.js +1 -0
  139. package/client/dist/assets/common-DnjcgkPH.js +1 -0
  140. package/client/dist/assets/common-GbpxfPG8.js +1 -0
  141. package/client/dist/assets/common-wA36jmj1.js +1 -0
  142. package/client/dist/assets/cpp-BVob6BaP.js +1 -0
  143. package/client/dist/assets/csharp-C4fbRuOu.js +1 -0
  144. package/client/dist/assets/csp-DthFP_vT.js +1 -0
  145. package/client/dist/assets/css-CGMH0hcW.js +3 -0
  146. package/client/dist/assets/css.worker-Wv5dxAWO.js +89 -0
  147. package/client/dist/assets/cssMode-Cc6ozl-J.js +1 -0
  148. package/client/dist/assets/cypher-Pnf68BRV.js +1 -0
  149. package/client/dist/assets/dart-PMMOtxZX.js +1 -0
  150. package/client/dist/assets/dashboard-B4ixDVk8.js +1 -0
  151. package/client/dist/assets/dashboard-BZBADHSj.js +1 -0
  152. package/client/dist/assets/dashboard-C1MfeUHs.js +1 -0
  153. package/client/dist/assets/dashboard-C7SK6xu5.js +1 -0
  154. package/client/dist/assets/dashboard-CB6Le1yN.js +1 -0
  155. package/client/dist/assets/dashboard-CoTpMOBM.js +1 -0
  156. package/client/dist/assets/dashboard-Duo4DDCW.js +1 -0
  157. package/client/dist/assets/dashboard-I19DXBxw.js +1 -0
  158. package/client/dist/assets/dist-js-BY-Fv_fg.js +1 -0
  159. package/client/dist/assets/dist-js-Bakc4uxT.js +1 -0
  160. package/client/dist/assets/dockerfile-di1nsJCc.js +1 -0
  161. package/client/dist/assets/ecl-D_WVcB5M.js +1 -0
  162. package/client/dist/assets/editor-Br_kD0ds.css +1 -0
  163. package/client/dist/assets/editor.api2-XLGzZfbc.js +872 -0
  164. package/client/dist/assets/editor.main-CfXxHimg.js +6 -0
  165. package/client/dist/assets/editor.worker-Bd9IXS8d.js +26 -0
  166. package/client/dist/assets/elixir-OAdJEMOn.js +1 -0
  167. package/client/dist/assets/explore-4mFpnrKU.js +1 -0
  168. package/client/dist/assets/explore-A8Ltoblq.js +1 -0
  169. package/client/dist/assets/explore-B9A3iN2W.js +1 -0
  170. package/client/dist/assets/explore-BV5Xxlsn.js +1 -0
  171. package/client/dist/assets/explore-BrBJvfjP.js +1 -0
  172. package/client/dist/assets/explore-C3FSE42C.js +1 -0
  173. package/client/dist/assets/explore-D2EFgt8J.js +1 -0
  174. package/client/dist/assets/explore-hFc3HFcp.js +1 -0
  175. package/client/dist/assets/flow9-D3QEZjgn.js +1 -0
  176. package/client/dist/assets/format-command-CwGuwzGA.js +1 -0
  177. package/client/dist/assets/freemarker2-DP7J1gG3.js +3 -0
  178. package/client/dist/assets/fsharp-BF0k_8N8.js +1 -0
  179. package/client/dist/assets/go-BAQO5Jsz.js +1 -0
  180. package/client/dist/assets/graphql-hdFVFkiV.js +1 -0
  181. package/client/dist/assets/handlebars-BjRlucw6.js +1 -0
  182. package/client/dist/assets/hcl-DWnl1o-X.js +1 -0
  183. package/client/dist/assets/html-OumBQJ-U.js +1 -0
  184. package/client/dist/assets/html.worker-CQP8QQsS.js +502 -0
  185. package/client/dist/assets/htmlMode-CStc3zXM.js +1 -0
  186. package/client/dist/assets/index-CimDRRi7.css +2 -0
  187. package/client/dist/assets/index-XGZaKl_u.js +142 -0
  188. package/client/dist/assets/ini-CB-6OVu3.js +1 -0
  189. package/client/dist/assets/integrations-C3p12Ms6.js +1 -0
  190. package/client/dist/assets/integrations-Cr6hH7XR.js +1 -0
  191. package/client/dist/assets/integrations-Cublz3m6.js +1 -0
  192. package/client/dist/assets/integrations-D28q1kF6.js +1 -0
  193. package/client/dist/assets/integrations-DRdbki5W.js +1 -0
  194. package/client/dist/assets/integrations-DaC4SzzL.js +1 -0
  195. package/client/dist/assets/integrations-DmQYCUvN.js +1 -0
  196. package/client/dist/assets/integrations-HIlUxXVs.js +1 -0
  197. package/client/dist/assets/java-d1CmfiHX.js +1 -0
  198. package/client/dist/assets/javascript-CMk--e7g.js +1 -0
  199. package/client/dist/assets/jobs-BE1siB0M.js +1 -0
  200. package/client/dist/assets/jobs-BHcQ_Faf.js +1 -0
  201. package/client/dist/assets/jobs-CFfc2dNX.js +1 -0
  202. package/client/dist/assets/jobs-CSi5n8X_.js +1 -0
  203. package/client/dist/assets/jobs-Dc3X86PY.js +1 -0
  204. package/client/dist/assets/jobs-De5tASex.js +1 -0
  205. package/client/dist/assets/jobs-DsoXEdo7.js +1 -0
  206. package/client/dist/assets/jobs-Wl-ApPMb.js +1 -0
  207. package/client/dist/assets/json.worker-DzV-CpCQ.js +58 -0
  208. package/client/dist/assets/jsonMode-C2h3ZcjZ.js +7 -0
  209. package/client/dist/assets/julia-Bgv08lKa.js +1 -0
  210. package/client/dist/assets/kotlin-u98kaVTf.js +1 -0
  211. package/client/dist/assets/less-CjYwpgg5.js +2 -0
  212. package/client/dist/assets/lexon-YTjaAFBB.js +1 -0
  213. package/client/dist/assets/lib-CPxTMOAq.js +7 -0
  214. package/client/dist/assets/liquid-mI3KJrBE.js +1 -0
  215. package/client/dist/assets/lspLanguageFeatures-DU09ggWi.js +4 -0
  216. package/client/dist/assets/lua-BzmkWv27.js +1 -0
  217. package/client/dist/assets/m3-CFwk9fw0.js +1 -0
  218. package/client/dist/assets/markdown-CR5iMpSZ.js +1 -0
  219. package/client/dist/assets/mdx-C41VDTR_.js +1 -0
  220. package/client/dist/assets/mips-CcEalc17.js +1 -0
  221. package/client/dist/assets/monaco.contribution-CPObAXMC.js +2 -0
  222. package/client/dist/assets/msdax-BQbkawnr.js +1 -0
  223. package/client/dist/assets/mysql-GTlaaW_P.js +1 -0
  224. package/client/dist/assets/nav-0fwkrgHt.js +1 -0
  225. package/client/dist/assets/nav-BEL3MTwK.js +1 -0
  226. package/client/dist/assets/nav-B_G-TJDW.js +1 -0
  227. package/client/dist/assets/nav-C2YXcbZS.js +1 -0
  228. package/client/dist/assets/nav-ClzOE4mA.js +1 -0
  229. package/client/dist/assets/nav-CtYwmMgu.js +1 -0
  230. package/client/dist/assets/nav-D2bOGSEg.js +1 -0
  231. package/client/dist/assets/nav-iH1V5j6o.js +1 -0
  232. package/client/dist/assets/objective-c-Byu1T5if.js +1 -0
  233. package/client/dist/assets/pascal-BrfzBfRm.js +1 -0
  234. package/client/dist/assets/pascaligo-BXXKFUeo.js +1 -0
  235. package/client/dist/assets/perl-B3OikKq-.js +1 -0
  236. package/client/dist/assets/pgsql-CTsa0Acc.js +1 -0
  237. package/client/dist/assets/php-DiQh3FUW.js +1 -0
  238. package/client/dist/assets/pla-92uH8Fzm.js +1 -0
  239. package/client/dist/assets/postiats-BbeWkKUr.js +1 -0
  240. package/client/dist/assets/powerquery-DgDMzpsm.js +1 -0
  241. package/client/dist/assets/powershell-BfdUUzaG.js +1 -0
  242. package/client/dist/assets/preload-helper-DSXbuxSR.js +1 -0
  243. package/client/dist/assets/protobuf-BojW2ftW.js +2 -0
  244. package/client/dist/assets/pug-BxqTg3IU.js +1 -0
  245. package/client/dist/assets/python-Y27rKQtk.js +1 -0
  246. package/client/dist/assets/qsharp-BX_A-MW9.js +1 -0
  247. package/client/dist/assets/r-D9BMnxvJ.js +1 -0
  248. package/client/dist/assets/razor-Cd5-q9Bp.js +1 -0
  249. package/client/dist/assets/redis-5cJqEQJJ.js +1 -0
  250. package/client/dist/assets/redshift-d8BBqiwb.js +1 -0
  251. package/client/dist/assets/restructuredtext-C8a6yIcZ.js +1 -0
  252. package/client/dist/assets/ruby-egeh-6KX.js +1 -0
  253. package/client/dist/assets/rust-a3r9IInB.js +1 -0
  254. package/client/dist/assets/sb-y8iRIDei.js +1 -0
  255. package/client/dist/assets/scala-BPDK2AmK.js +1 -0
  256. package/client/dist/assets/scheme-BIWUEoOs.js +1 -0
  257. package/client/dist/assets/scss-CA-PSzwg.js +3 -0
  258. package/client/dist/assets/settings-55oDcbSh.js +1 -0
  259. package/client/dist/assets/settings-Bd4Tq1RB.js +1 -0
  260. package/client/dist/assets/settings-CCSM-Fhn.js +1 -0
  261. package/client/dist/assets/settings-D3e_bDoW.js +1 -0
  262. package/client/dist/assets/settings-DKbTkbn7.js +1 -0
  263. package/client/dist/assets/settings-Dxpo6_w7.js +1 -0
  264. package/client/dist/assets/settings-bt84e3Aa.js +1 -0
  265. package/client/dist/assets/settings-nu68QukM.js +1 -0
  266. package/client/dist/assets/setup-BMqwfbW9.js +1 -0
  267. package/client/dist/assets/setup-Bb5LcG28.js +1 -0
  268. package/client/dist/assets/setup-BeEx2_da.js +1 -0
  269. package/client/dist/assets/setup-CCCrB53Q.js +1 -0
  270. package/client/dist/assets/setup-CJA0ATmd.js +1 -0
  271. package/client/dist/assets/setup-CeiDbZcb.js +1 -0
  272. package/client/dist/assets/setup-Cus7TApA.js +1 -0
  273. package/client/dist/assets/setup-D9qOs2Xo.js +1 -0
  274. package/client/dist/assets/shell--LiT1Bja.js +1 -0
  275. package/client/dist/assets/solidity-DdqZccZg.js +1 -0
  276. package/client/dist/assets/sophia-S6-YxNG_.js +1 -0
  277. package/client/dist/assets/sparql-BSf5kMp2.js +1 -0
  278. package/client/dist/assets/specs-BFfu3u-a.js +1 -0
  279. package/client/dist/assets/specs-B__C8-8a.js +1 -0
  280. package/client/dist/assets/specs-CZ1PsXsC.js +1 -0
  281. package/client/dist/assets/specs-D2FzlLn9.js +1 -0
  282. package/client/dist/assets/specs-DaUTrNF9.js +1 -0
  283. package/client/dist/assets/specs-Dyc5hYeE.js +1 -0
  284. package/client/dist/assets/specs-cKEh2LXt.js +1 -0
  285. package/client/dist/assets/specs-k0PyLDVt.js +1 -0
  286. package/client/dist/assets/sql-D7KgjR8G.js +1 -0
  287. package/client/dist/assets/st-BnoDa-Ml.js +1 -0
  288. package/client/dist/assets/swift-DEUHTkUX.js +1 -0
  289. package/client/dist/assets/systemverilog-Tqb_KPnW.js +1 -0
  290. package/client/dist/assets/tcl-BmBFS2qq.js +1 -0
  291. package/client/dist/assets/terminal-80yDMgMF.js +1 -0
  292. package/client/dist/assets/terminal-Bje4ziIa.js +1 -0
  293. package/client/dist/assets/terminal-C2WYcFHF.js +1 -0
  294. package/client/dist/assets/terminal-CSONJOex.js +1 -0
  295. package/client/dist/assets/terminal-DEqzGtcr.js +1 -0
  296. package/client/dist/assets/terminal-DeWzh6ys.js +1 -0
  297. package/client/dist/assets/terminal-YOlsJCQj.js +1 -0
  298. package/client/dist/assets/terminal-lkZYR4wJ.js +1 -0
  299. package/client/dist/assets/tickets-CB7N30gm.js +1 -0
  300. package/client/dist/assets/tickets-CF2PYelu.js +1 -0
  301. package/client/dist/assets/tickets-DNOANUXr.js +1 -0
  302. package/client/dist/assets/tickets-DU1aqsbr.js +1 -0
  303. package/client/dist/assets/tickets-DYvafSaY.js +1 -0
  304. package/client/dist/assets/tickets-DlpC_iTg.js +1 -0
  305. package/client/dist/assets/tickets-DucYgtdl.js +1 -0
  306. package/client/dist/assets/tickets-clefmXLv.js +1 -0
  307. package/client/dist/assets/ts.worker-METxwbDZ.js +67719 -0
  308. package/client/dist/assets/tsMode-B0y_xEci.js +11 -0
  309. package/client/dist/assets/twig-BQV8igWC.js +1 -0
  310. package/client/dist/assets/typescript-BzK0OgwW.js +1 -0
  311. package/client/dist/assets/typespec-DlFroUGY.js +1 -0
  312. package/client/dist/assets/useProjectCache-DSaiGFjV.js +1 -0
  313. package/client/dist/assets/vb-BlrJpIMX.js +1 -0
  314. package/client/dist/assets/wgsl-BWgIc6FZ.js +298 -0
  315. package/client/dist/assets/workers-rt--R2Qy.js +1 -0
  316. package/client/dist/assets/xml-eX9QXAmI.js +1 -0
  317. package/client/dist/assets/yaml-fcsNkpOt.js +1 -0
  318. package/client/dist/index.html +246 -0
  319. package/docs/README.md +54 -0
  320. package/docs/cli.md +198 -0
  321. package/docs/codex.md +210 -0
  322. package/docs/creating-specs.md +197 -0
  323. package/docs/customizing.md +197 -0
  324. package/docs/getting-started.md +140 -0
  325. package/docs/internals/README.md +25 -0
  326. package/docs/internals/adding-a-provider.md +238 -0
  327. package/docs/internals/api-reference.md +634 -0
  328. package/docs/internals/architecture.md +332 -0
  329. package/docs/internals/configuration.md +172 -0
  330. package/docs/internals/openspec-workflow.md +282 -0
  331. package/docs/internals/operations-runbook.md +198 -0
  332. package/docs/internals/profiles.md +152 -0
  333. package/docs/platforms/macos.md +130 -0
  334. package/docs/platforms/windows.md +81 -0
  335. package/docs/running-pipelines.md +240 -0
  336. package/docs/terminal.md +138 -0
  337. package/docs/tracking-cost.md +155 -0
  338. package/package.json +82 -0
  339. package/server/dist/agent-generator.js +232 -0
  340. package/server/dist/agent-refine-db.js +124 -0
  341. package/server/dist/agent-refine-manager.js +526 -0
  342. package/server/dist/ai-invocations.js +111 -0
  343. package/server/dist/attachment-manager.js +299 -0
  344. package/server/dist/auth.js +207 -0
  345. package/server/dist/binary-probe.js +35 -0
  346. package/server/dist/browser-capture-manager.js +576 -0
  347. package/server/dist/browser-capture-types.js +28 -0
  348. package/server/dist/browser-network.js +149 -0
  349. package/server/dist/browser-playwright.js +888 -0
  350. package/server/dist/build-dirs.js +44 -0
  351. package/server/dist/changes-reader.js +120 -0
  352. package/server/dist/chat-manager.js +1060 -0
  353. package/server/dist/chromium-resolver.js +311 -0
  354. package/server/dist/code-explorer-router.js +788 -0
  355. package/server/dist/codex-otel-bridge.js +235 -0
  356. package/server/dist/command-resolver.js +102 -0
  357. package/server/dist/config.js +306 -0
  358. package/server/dist/context-budget.js +113 -0
  359. package/server/dist/context-scope.js +279 -0
  360. package/server/dist/contract-refine-runner.js +521 -0
  361. package/server/dist/core-compat.js +207 -0
  362. package/server/dist/core-package.js +14 -0
  363. package/server/dist/db.js +1034 -0
  364. package/server/dist/desktop-analytics.js +156 -0
  365. package/server/dist/desktop-db.js +456 -0
  366. package/server/dist/desktop-router.js +735 -0
  367. package/server/dist/docs-router.js +207 -0
  368. package/server/dist/explore-contract-refine.js +421 -0
  369. package/server/dist/explore-cwd-manager.js +242 -0
  370. package/server/dist/explore-draft-title.js +47 -0
  371. package/server/dist/explore-smash.js +450 -0
  372. package/server/dist/feature-flags.js +17 -0
  373. package/server/dist/file-provenance.js +382 -0
  374. package/server/dist/file-summary-generator.js +221 -0
  375. package/server/dist/file-summary-manager.js +689 -0
  376. package/server/dist/hooks.js +102 -0
  377. package/server/dist/ids.js +7 -0
  378. package/server/dist/index.js +586 -0
  379. package/server/dist/metrics.js +136 -0
  380. package/server/dist/mobile/index.js +16 -0
  381. package/server/dist/mobile/mobile-admin-router.js +84 -0
  382. package/server/dist/mobile/mobile-auth.js +67 -0
  383. package/server/dist/mobile/mobile-devices.js +80 -0
  384. package/server/dist/mobile/mobile-event-bus.js +39 -0
  385. package/server/dist/mobile/mobile-gateway.js +285 -0
  386. package/server/dist/mobile/mobile-mdns.js +81 -0
  387. package/server/dist/mobile/mobile-pairing.js +179 -0
  388. package/server/dist/mobile/mobile-redact.js +53 -0
  389. package/server/dist/mobile/mobile-router.js +411 -0
  390. package/server/dist/mobile/mobile-tls.js +86 -0
  391. package/server/dist/mobile/mobile-types.js +9 -0
  392. package/server/dist/mobile/mobile-ws.js +275 -0
  393. package/server/dist/path-resolver.js +298 -0
  394. package/server/dist/plugin-manager.js +617 -0
  395. package/server/dist/plugins/claude-approval.js +179 -0
  396. package/server/dist/plugins/claude-md-mutation.js +146 -0
  397. package/server/dist/plugins/codex-mcp.js +108 -0
  398. package/server/dist/plugins/contributors.js +72 -0
  399. package/server/dist/plugins/drift.js +58 -0
  400. package/server/dist/plugins/index.js +14 -0
  401. package/server/dist/plugins/json-mutation.js +120 -0
  402. package/server/dist/plugins/manager.js +32 -0
  403. package/server/dist/plugins/ownership.js +86 -0
  404. package/server/dist/plugins/paths.js +37 -0
  405. package/server/dist/plugins/prereq-installer.js +104 -0
  406. package/server/dist/plugins/rail-integration.js +79 -0
  407. package/server/dist/plugins/serena/index.js +13 -0
  408. package/server/dist/plugins/serena/install.js +91 -0
  409. package/server/dist/plugins/serena/instructions-content.js +21 -0
  410. package/server/dist/plugins/serena/manifest.js +111 -0
  411. package/server/dist/plugins/serena/verify.js +78 -0
  412. package/server/dist/plugins-router.js +215 -0
  413. package/server/dist/pricing.js +89 -0
  414. package/server/dist/profile-manager.js +310 -0
  415. package/server/dist/profiles-router.js +759 -0
  416. package/server/dist/project-registry.js +443 -0
  417. package/server/dist/project-router.js +4016 -0
  418. package/server/dist/proposal-manager.js +291 -0
  419. package/server/dist/provider-selection.js +69 -0
  420. package/server/dist/providers/claude-adapter.js +281 -0
  421. package/server/dist/providers/codex-adapter.js +264 -0
  422. package/server/dist/providers/index.js +23 -0
  423. package/server/dist/providers/registry.js +37 -0
  424. package/server/dist/providers/types.js +22 -0
  425. package/server/dist/queue-manager.js +1511 -0
  426. package/server/dist/rails-router.js +362 -0
  427. package/server/dist/rails-store.js +116 -0
  428. package/server/dist/result-event.js +106 -0
  429. package/server/dist/schemas/profile.v1.json +151 -0
  430. package/server/dist/setup-manager.js +1165 -0
  431. package/server/dist/setup-prerequisites.js +372 -0
  432. package/server/dist/smash-runner.js +663 -0
  433. package/server/dist/spec-draft-parser.js +133 -0
  434. package/server/dist/spec-launcher-manager.js +174 -0
  435. package/server/dist/spec-models.js +32 -0
  436. package/server/dist/specrails-tech-client.js +82 -0
  437. package/server/dist/spending.js +448 -0
  438. package/server/dist/telemetry-compactor.js +180 -0
  439. package/server/dist/telemetry-export.js +317 -0
  440. package/server/dist/telemetry-receiver.js +224 -0
  441. package/server/dist/terminal-manager.js +633 -0
  442. package/server/dist/terminal-marks-store.js +117 -0
  443. package/server/dist/terminal-osc-parser.js +159 -0
  444. package/server/dist/terminal-settings.js +282 -0
  445. package/server/dist/terminal-shell-integration.js +196 -0
  446. package/server/dist/ticket-broadcast.js +47 -0
  447. package/server/dist/ticket-store.js +397 -0
  448. package/server/dist/ticket-watcher.js +117 -0
  449. package/server/dist/types.js +10 -0
  450. package/server/dist/user-mcp-config.js +117 -0
  451. package/server/dist/util/cli-prompt.js +181 -0
  452. package/server/dist/util/secure-fs.js +50 -0
  453. package/server/dist/util/win-spawn.js +43 -0
  454. package/server/dist/webhook-manager.js +89 -0
  455. package/server/dist/ws-routing.js +47 -0
@@ -0,0 +1,788 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createCodeExplorerRouter = createCodeExplorerRouter;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const express_1 = require("express");
11
+ const feature_flags_1 = require("./feature-flags");
12
+ const build_dirs_1 = require("./build-dirs");
13
+ const file_provenance_1 = require("./file-provenance");
14
+ const file_summary_manager_1 = require("./file-summary-manager");
15
+ const MAX_TREE_PAGE = 2000;
16
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
17
+ const BINARY_PROBE_BYTES = 8 * 1024;
18
+ // Hard-coded app deny-list (mirrors design D8). Dotfiles are excluded by name
19
+ // prefix; build/dep dirs come from the shared BUILD_DIRS set (node_modules, dist,
20
+ // build, out, coverage, target, vendor) so the on-demand tree walk skips the same
21
+ // heavy trees the file-summary watcher prunes; extensions handled below.
22
+ const DENY_EXTS = new Set(['.lock', '.log']);
23
+ // Secret-bearing extensions/names blocked as defense-in-depth so credentials are
24
+ // never served to the (non-developer) reader even if .gitignore is missing or git
25
+ // is unavailable. Kept conservative to avoid hiding ordinary source files.
26
+ const SECRET_EXTS = new Set(['.pem', '.key', '.p12', '.pfx', '.keystore', '.jks']);
27
+ const SECRET_NAMES = new Set(['id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519']);
28
+ const DENY_NAMES = new Set(['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']);
29
+ function isDenied(entryName) {
30
+ // Dotfiles (.env, .npmrc, .netrc, .git, …) are denied wholesale by prefix.
31
+ if (entryName.startsWith('.'))
32
+ return true;
33
+ // Case-insensitive for dir/lockfile names: macOS (APFS) and Windows (NTFS) are
34
+ // case-insensitive, so `Node_Modules` / `Package-Lock.json` resolve to the same
35
+ // denied path on disk and must not slip past the policy.
36
+ const lower = entryName.toLowerCase();
37
+ if (build_dirs_1.BUILD_DIRS.has(lower))
38
+ return true;
39
+ const ext = path_1.default.extname(lower);
40
+ if (DENY_EXTS.has(ext) || SECRET_EXTS.has(ext))
41
+ return true;
42
+ if (DENY_NAMES.has(lower) || SECRET_NAMES.has(lower))
43
+ return true;
44
+ return false;
45
+ }
46
+ // Apply the deny-list to ANY segment of a relative path so the policy is the
47
+ // single source of truth across every surface (tree walk, touched-by-ai list,
48
+ // and the content endpoints) — not just the top-level `all` walk.
49
+ function isDeniedRelPath(rel) {
50
+ return rel.split(/[\\/]/).filter(Boolean).some(isDenied);
51
+ }
52
+ // Normalize a client-supplied relative path to POSIX separators so summary
53
+ // (sha256 of relPath), provenance (git always emits '/'), and content lookups
54
+ // all key off ONE canonical form regardless of the request's separator style.
55
+ function normalizeRel(rel) {
56
+ return rel.split(/[\\/]/).filter((seg) => seg.length > 0).join('/');
57
+ }
58
+ // Return the subset of `relPaths` that git considers ignored (honours nested
59
+ // .gitignore, excludes tracked files — exactly the set we must hide). One batched
60
+ // `git check-ignore` spawn. Best-effort: any git failure (no repo, no git) → no
61
+ // paths reported, so the deny-list remains the only filter. `check-ignore` exits
62
+ // 1 ("none ignored") which execFileSync throws on — the matched list still lands
63
+ // on stdout, so both branches read stdout.
64
+ function gitIgnoredSet(projectPath, relPaths) {
65
+ if (relPaths.length === 0)
66
+ return new Set();
67
+ let out = '';
68
+ try {
69
+ out = (0, child_process_1.execFileSync)('git', ['check-ignore', '--stdin', '-z'], {
70
+ cwd: projectPath,
71
+ input: relPaths.join('\0') + '\0',
72
+ encoding: 'utf8',
73
+ stdio: ['pipe', 'pipe', 'ignore'],
74
+ maxBuffer: 16 * 1024 * 1024,
75
+ timeout: 15_000,
76
+ });
77
+ }
78
+ catch (err) {
79
+ out = (err.stdout ?? '').toString();
80
+ }
81
+ return new Set(out.split('\0').filter((p) => p.length > 0));
82
+ }
83
+ function isGitIgnored(projectPath, relPath) {
84
+ return gitIgnoredSet(projectPath, [relPath]).has(relPath);
85
+ }
86
+ function languageForExt(ext) {
87
+ const e = ext.toLowerCase();
88
+ switch (e) {
89
+ case '.ts':
90
+ case '.tsx':
91
+ case '.cts':
92
+ case '.mts': return 'typescript';
93
+ case '.js':
94
+ case '.jsx':
95
+ case '.mjs':
96
+ case '.cjs': return 'javascript';
97
+ case '.json': return 'json';
98
+ case '.md': return 'markdown';
99
+ case '.py': return 'python';
100
+ case '.rs': return 'rust';
101
+ case '.go': return 'go';
102
+ case '.css': return 'css';
103
+ case '.html': return 'html';
104
+ case '.yml':
105
+ case '.yaml': return 'yaml';
106
+ case '.sh': return 'shell';
107
+ case '.sql': return 'sql';
108
+ case '.toml': return 'toml';
109
+ default: return 'plaintext';
110
+ }
111
+ }
112
+ function decodeCursor(raw) {
113
+ if (!raw)
114
+ return { skip: 0 };
115
+ try {
116
+ const json = Buffer.from(raw, 'base64').toString('utf8');
117
+ const parsed = JSON.parse(json);
118
+ if (typeof parsed.skip === 'number' && parsed.skip >= 0)
119
+ return { skip: parsed.skip };
120
+ }
121
+ catch {
122
+ // fall through to default
123
+ }
124
+ return { skip: 0 };
125
+ }
126
+ function encodeCursor(skip) {
127
+ return Buffer.from(JSON.stringify({ skip }), 'utf8').toString('base64');
128
+ }
129
+ function rollupProvenance(rows) {
130
+ let createdByTicketId = null;
131
+ const modifiedSet = new Set();
132
+ // `rows` arrives ordered by `at DESC`. Walk oldest → newest so the earliest
133
+ // 'created' wins for createdByTicketId.
134
+ for (const r of [...rows].reverse()) {
135
+ if (r.ticket_id == null)
136
+ continue;
137
+ if (r.kind === 'created' && createdByTicketId == null) {
138
+ createdByTicketId = r.ticket_id;
139
+ }
140
+ else if (r.kind === 'modified') {
141
+ modifiedSet.add(r.ticket_id);
142
+ }
143
+ }
144
+ // Don't double-count the creating ticket in the modified chips list.
145
+ if (createdByTicketId != null)
146
+ modifiedSet.delete(createdByTicketId);
147
+ return {
148
+ createdByTicketId,
149
+ modifiedByTicketIds: [...modifiedSet],
150
+ latest: rows[0] ?? null,
151
+ touchedFileCount: 0,
152
+ rows,
153
+ };
154
+ }
155
+ function rollupDirectoryProvenance(rowsByPath, dirPath) {
156
+ const prefix = `${dirPath}/`;
157
+ const childRows = [];
158
+ let touchedFileCount = 0;
159
+ for (const [filePath, rows] of rowsByPath) {
160
+ if (!filePath.startsWith(prefix))
161
+ continue;
162
+ touchedFileCount += 1;
163
+ childRows.push(...rows);
164
+ }
165
+ childRows.sort((a, b) => b.at - a.at);
166
+ return {
167
+ ...rollupProvenance(childRows),
168
+ touchedFileCount,
169
+ };
170
+ }
171
+ function provenanceToJson(row) {
172
+ if (!row)
173
+ return null;
174
+ return {
175
+ path: row.file_path,
176
+ ticketId: row.ticket_id,
177
+ jobId: row.job_id,
178
+ kind: row.kind,
179
+ at: row.at,
180
+ };
181
+ }
182
+ function provenanceRowsToJson(rows) {
183
+ return rows.map((row) => ({
184
+ path: row.file_path,
185
+ ticketId: row.ticket_id,
186
+ jobId: row.job_id,
187
+ kind: row.kind,
188
+ at: row.at,
189
+ }));
190
+ }
191
+ function treeProvenanceToJson(provenance) {
192
+ return {
193
+ createdByTicketId: provenance.createdByTicketId,
194
+ modifiedByTicketIds: provenance.modifiedByTicketIds,
195
+ latest: provenanceToJson(provenance.latest),
196
+ touchedFileCount: provenance.touchedFileCount,
197
+ rows: provenanceRowsToJson(provenance.rows),
198
+ };
199
+ }
200
+ function parsePositiveInt(raw) {
201
+ if (typeof raw !== 'string' || raw.trim() === '')
202
+ return null;
203
+ const n = Number(raw);
204
+ return Number.isInteger(n) && n > 0 ? n : null;
205
+ }
206
+ function parseNonEmptyString(raw) {
207
+ return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : null;
208
+ }
209
+ function listTouchedRows(db, filters) {
210
+ const where = [];
211
+ const args = [];
212
+ if (filters.ticketId != null) {
213
+ where.push('ticket_id = ?');
214
+ args.push(filters.ticketId);
215
+ }
216
+ if (filters.jobId) {
217
+ where.push('job_id = ?');
218
+ args.push(filters.jobId);
219
+ }
220
+ if (filters.path) {
221
+ where.push('file_path = ?');
222
+ args.push(filters.path);
223
+ }
224
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
225
+ return db.prepare(`SELECT id, file_path, ticket_id, job_id, kind, at
226
+ FROM file_provenance ${whereSql}
227
+ ORDER BY file_path ASC, at DESC`).all(...args);
228
+ }
229
+ function listAllEntries(projectPath) {
230
+ const out = [];
231
+ const stack = [projectPath];
232
+ while (stack.length > 0) {
233
+ const dir = stack.pop();
234
+ let entries;
235
+ try {
236
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
237
+ }
238
+ catch {
239
+ continue;
240
+ }
241
+ for (const entry of entries) {
242
+ if (isDenied(entry.name))
243
+ continue;
244
+ const abs = path_1.default.join(dir, entry.name);
245
+ // Normalize to POSIX so provenance/summary lookups (which are '/'-keyed)
246
+ // match on Windows, where path.relative emits backslashes.
247
+ const rel = normalizeRel(path_1.default.relative(projectPath, abs));
248
+ if (entry.isDirectory()) {
249
+ out.push({ rel, isDir: true, size: null, mtime: null });
250
+ stack.push(abs);
251
+ }
252
+ else if (entry.isFile()) {
253
+ let size = null;
254
+ let mtime = null;
255
+ try {
256
+ const st = fs_1.default.statSync(abs);
257
+ size = st.size;
258
+ mtime = st.mtimeMs;
259
+ }
260
+ catch {
261
+ // ignore
262
+ }
263
+ out.push({ rel, isDir: false, size, mtime });
264
+ }
265
+ }
266
+ }
267
+ // Drop gitignored files (honours the documented .gitignore-respect contract).
268
+ // Directories are kept — git can't report an ignored dir without its files, and
269
+ // an empty dir node is harmless; its ignored children are already filtered.
270
+ const files = out.filter((e) => !e.isDir).map((e) => e.rel);
271
+ const ignored = gitIgnoredSet(projectPath, files);
272
+ const filtered = ignored.size > 0 ? out.filter((e) => e.isDir || !ignored.has(e.rel)) : out;
273
+ filtered.sort((a, b) => a.rel.localeCompare(b.rel));
274
+ return filtered;
275
+ }
276
+ function listTouchedEntries(projectPath, rowsByPath) {
277
+ const seen = new Set();
278
+ const out = [];
279
+ for (const filePath of rowsByPath.keys()) {
280
+ // Keep touched-by-ai consistent with the `all` tree (and never surface
281
+ // secrets like .env that an AI job happened to touch).
282
+ if (isDeniedRelPath(filePath))
283
+ continue;
284
+ const parts = filePath.split('/').filter(Boolean);
285
+ for (let i = 1; i < parts.length; i += 1) {
286
+ const dirRel = parts.slice(0, i).join('/');
287
+ if (!seen.has(dirRel)) {
288
+ seen.add(dirRel);
289
+ out.push({ rel: dirRel, isDir: true, size: null, mtime: null });
290
+ }
291
+ }
292
+ if (seen.has(filePath))
293
+ continue;
294
+ seen.add(filePath);
295
+ const abs = path_1.default.join(projectPath, filePath);
296
+ let size = null;
297
+ let mtime = null;
298
+ try {
299
+ const st = fs_1.default.statSync(abs);
300
+ size = st.size;
301
+ mtime = st.mtimeMs;
302
+ }
303
+ catch {
304
+ // file may have been deleted after provenance was recorded
305
+ }
306
+ out.push({ rel: filePath, isDir: false, size, mtime });
307
+ }
308
+ out.sort((a, b) => {
309
+ const byPath = a.rel.localeCompare(b.rel);
310
+ if (byPath !== 0)
311
+ return byPath;
312
+ return Number(b.isDir) - Number(a.isDir);
313
+ });
314
+ return out;
315
+ }
316
+ // Set of summary file basenames (without `.json`), i.e. the path-hash of every
317
+ // file that currently has a summary on disk. One readdir replaces a per-entry
318
+ // readSummary disk hit during the tree walk.
319
+ function readSummaryHashSet(projectPath) {
320
+ const set = new Set();
321
+ let files;
322
+ try {
323
+ files = fs_1.default.readdirSync((0, file_summary_manager_1.summariesDir)(projectPath));
324
+ }
325
+ catch {
326
+ return set;
327
+ }
328
+ for (const f of files) {
329
+ if (f.endsWith('.json'))
330
+ set.add(f.slice(0, -'.json'.length));
331
+ }
332
+ return set;
333
+ }
334
+ function createCodeExplorerRouter(deps) {
335
+ const router = (0, express_1.Router)({ mergeParams: true });
336
+ const listByPath = deps.listProvenanceByPath ?? file_provenance_1.listProvenanceByPath;
337
+ const listByTicket = deps.listProvenanceByTicket ?? file_provenance_1.listProvenanceByTicket;
338
+ // Short-TTL per-project cache so paginating a large `all` tree reuses ONE
339
+ // synchronous filesystem walk instead of re-walking (and re-statting) on every
340
+ // page — the cursor only slices an already-materialized array. Also caches the
341
+ // one-readdir summary-hash set. 5s is long enough for a pagination burst and
342
+ // short enough that tree edits surface promptly.
343
+ const WALK_CACHE_TTL_MS = 5000;
344
+ let allEntriesCache = null;
345
+ let summaryHashCache = null;
346
+ const nowMs = () => Date.now();
347
+ function getAllEntriesCached() {
348
+ if (allEntriesCache && nowMs() - allEntriesCache.at < WALK_CACHE_TTL_MS)
349
+ return allEntriesCache.entries;
350
+ const entries = listAllEntries(deps.projectPath);
351
+ allEntriesCache = { at: nowMs(), entries };
352
+ return entries;
353
+ }
354
+ function getSummaryHashesCached() {
355
+ if (summaryHashCache && nowMs() - summaryHashCache.at < WALK_CACHE_TTL_MS)
356
+ return summaryHashCache.set;
357
+ const set = readSummaryHashSet(deps.projectPath);
358
+ summaryHashCache = { at: nowMs(), set };
359
+ return set;
360
+ }
361
+ // Feature-flag gate — entire prefix returns 404 when disabled.
362
+ router.use((_req, res, next) => {
363
+ if (!(0, feature_flags_1.isCodeExplorerEnabled)()) {
364
+ res.status(404).end();
365
+ return;
366
+ }
367
+ // Lazily attach the file-summary watcher on first Code-Explorer use. It is
368
+ // not attached at registry load (that recursive watcher caused the fd leak
369
+ // that broke terminals); attachWatcher is idempotent, so this is cheap on
370
+ // every subsequent request.
371
+ try {
372
+ deps.fileSummaryManager.attachWatcher(deps.projectId, deps.projectPath);
373
+ }
374
+ catch { /* non-fatal */ }
375
+ next();
376
+ });
377
+ router.get('/tree', (req, res) => {
378
+ const filter = req.query.filter ?? 'touched-by-ai';
379
+ const withProvenance = req.query.withProvenance === '1' || req.query.withProvenance === 'true';
380
+ const { skip } = decodeCursor(req.query.cursor);
381
+ const ticketId = parsePositiveInt(req.query.ticketId);
382
+ const jobId = parseNonEmptyString(req.query.jobId);
383
+ let entries;
384
+ const touchedRowsByPath = new Map();
385
+ if (filter === 'touched-by-ai') {
386
+ const rows = listTouchedRows(deps.db, { ticketId, jobId });
387
+ for (const row of rows) {
388
+ const existing = touchedRowsByPath.get(row.file_path);
389
+ if (existing)
390
+ existing.push(row);
391
+ else
392
+ touchedRowsByPath.set(row.file_path, [row]);
393
+ }
394
+ entries = listTouchedEntries(deps.projectPath, touchedRowsByPath);
395
+ }
396
+ else {
397
+ entries = getAllEntriesCached();
398
+ // Batch-load ALL provenance once instead of a per-entry SQL query (N+1).
399
+ if (withProvenance) {
400
+ for (const row of listTouchedRows(deps.db, {})) {
401
+ const existing = touchedRowsByPath.get(row.file_path);
402
+ if (existing)
403
+ existing.push(row);
404
+ else
405
+ touchedRowsByPath.set(row.file_path, [row]);
406
+ }
407
+ }
408
+ }
409
+ const page = entries.slice(skip, skip + MAX_TREE_PAGE);
410
+ const nextCursor = skip + page.length < entries.length ? encodeCursor(skip + page.length) : null;
411
+ // Read the summaries dir ONCE into a Set of path-hashes instead of opening +
412
+ // parsing a JSON file per entry just to test existence (cached per project).
413
+ const summaryHashes = getSummaryHashesCached();
414
+ const out = page.map((e) => {
415
+ const isTouchedDir = filter === 'touched-by-ai' && e.isDir;
416
+ const rawRows = withProvenance && !isTouchedDir ? (touchedRowsByPath.get(e.rel) ?? []) : [];
417
+ const provenance = withProvenance && isTouchedDir
418
+ ? rollupDirectoryProvenance(touchedRowsByPath, e.rel)
419
+ : rollupProvenance(rawRows);
420
+ return {
421
+ path: e.rel,
422
+ kind: e.isDir ? 'dir' : 'file',
423
+ sizeBytes: e.size,
424
+ hasSummary: !e.isDir && summaryHashes.has((0, file_summary_manager_1.pathHash)(e.rel)),
425
+ provenance,
426
+ lastModifiedAt: e.mtime,
427
+ };
428
+ });
429
+ res.json({
430
+ entries: out.map((entry) => ({
431
+ ...entry,
432
+ provenance: treeProvenanceToJson(entry.provenance),
433
+ })),
434
+ nextCursor,
435
+ });
436
+ });
437
+ router.get('/file', async (req, res) => {
438
+ const relRaw = req.query.path;
439
+ if (!relRaw || typeof relRaw !== 'string') {
440
+ res.status(400).json({ error: 'path query parameter is required' });
441
+ return;
442
+ }
443
+ const guard = resolveSafePath(deps.projectPath, relRaw);
444
+ if (!guard) {
445
+ res.status(400).json({ error: 'path traversal not allowed' });
446
+ return;
447
+ }
448
+ if (isDeniedRelPath(relRaw)) {
449
+ res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
450
+ return;
451
+ }
452
+ // Canonical POSIX form for all summary/provenance/hash lookups.
453
+ const rel = normalizeRel(relRaw);
454
+ if (isGitIgnored(deps.projectPath, rel)) {
455
+ res.status(403).json({ error: 'path is gitignored' });
456
+ return;
457
+ }
458
+ const abs = guard;
459
+ let stat;
460
+ try {
461
+ stat = fs_1.default.statSync(abs);
462
+ }
463
+ catch {
464
+ // Honour the staleness scenario: even if content is unavailable, return
465
+ // the existing summary so the client can render a "not found" banner.
466
+ const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
467
+ const provenance = listByPath(deps.db, deps.projectId, rel);
468
+ if (summary || provenance.length > 0) {
469
+ res.json({
470
+ content: null,
471
+ reason: 'not-found',
472
+ summary,
473
+ summaryStale: true,
474
+ provenance: provenanceRowsToJson(provenance),
475
+ });
476
+ return;
477
+ }
478
+ res.status(404).json({ error: 'file not found' });
479
+ return;
480
+ }
481
+ if (!stat.isFile()) {
482
+ res.status(400).json({ error: 'path is not a regular file' });
483
+ return;
484
+ }
485
+ if (stat.size > MAX_FILE_BYTES) {
486
+ res.json({
487
+ tooLarge: true,
488
+ sizeBytes: stat.size,
489
+ provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
490
+ summary: (0, file_summary_manager_1.readSummary)(deps.projectPath, rel),
491
+ absolutePath: abs,
492
+ });
493
+ return;
494
+ }
495
+ // Binary detection: read first 8 KB, scan for NUL.
496
+ let head;
497
+ try {
498
+ const fd = fs_1.default.openSync(abs, 'r');
499
+ try {
500
+ head = Buffer.alloc(Math.min(BINARY_PROBE_BYTES, stat.size));
501
+ fs_1.default.readSync(fd, head, 0, head.length, 0);
502
+ }
503
+ finally {
504
+ fs_1.default.closeSync(fd);
505
+ }
506
+ }
507
+ catch {
508
+ res.status(500).json({ error: 'failed to read file' });
509
+ return;
510
+ }
511
+ if (head.includes(0)) {
512
+ res.json({
513
+ binary: true,
514
+ sizeBytes: stat.size,
515
+ mime: 'application/octet-stream',
516
+ provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
517
+ summary: (0, file_summary_manager_1.readSummary)(deps.projectPath, rel),
518
+ absolutePath: abs,
519
+ });
520
+ return;
521
+ }
522
+ let content;
523
+ try {
524
+ content = fs_1.default.readFileSync(abs, 'utf8');
525
+ }
526
+ catch {
527
+ res.status(500).json({ error: 'failed to read file' });
528
+ return;
529
+ }
530
+ const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
531
+ const summaryStale = await computeStaleness(abs, summary);
532
+ res.json({
533
+ content,
534
+ encoding: 'utf-8',
535
+ language: languageForExt(path_1.default.extname(rel)),
536
+ provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
537
+ summary,
538
+ summaryStale,
539
+ absolutePath: abs,
540
+ });
541
+ });
542
+ router.get('/summary', async (req, res) => {
543
+ const relRaw = req.query.path;
544
+ if (!relRaw || typeof relRaw !== 'string') {
545
+ res.status(400).json({ error: 'path query parameter is required' });
546
+ return;
547
+ }
548
+ const guard = resolveSafePath(deps.projectPath, relRaw);
549
+ if (!guard) {
550
+ res.status(400).json({ error: 'path traversal not allowed' });
551
+ return;
552
+ }
553
+ if (isDeniedRelPath(relRaw)) {
554
+ res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
555
+ return;
556
+ }
557
+ const rel = normalizeRel(relRaw);
558
+ if (isGitIgnored(deps.projectPath, rel)) {
559
+ res.status(403).json({ error: 'path is gitignored' });
560
+ return;
561
+ }
562
+ const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
563
+ if (!summary) {
564
+ res.json({ summary: null });
565
+ return;
566
+ }
567
+ let summaryStale = false;
568
+ try {
569
+ summaryStale = await computeStaleness(guard, summary);
570
+ }
571
+ catch {
572
+ summaryStale = true;
573
+ }
574
+ res.json({ summary, summaryStale });
575
+ });
576
+ router.post('/file/regenerate-summary', async (req, res) => {
577
+ const relRaw = req.query.path;
578
+ if (!relRaw || typeof relRaw !== 'string') {
579
+ res.status(400).json({ error: 'path query parameter is required' });
580
+ return;
581
+ }
582
+ const guard = resolveSafePath(deps.projectPath, relRaw);
583
+ if (!guard) {
584
+ res.status(400).json({ error: 'path traversal not allowed' });
585
+ return;
586
+ }
587
+ if (isDeniedRelPath(relRaw)) {
588
+ res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
589
+ return;
590
+ }
591
+ const rel = normalizeRel(relRaw);
592
+ if (isGitIgnored(deps.projectPath, rel)) {
593
+ res.status(403).json({ error: 'path is gitignored' });
594
+ return;
595
+ }
596
+ let stat;
597
+ try {
598
+ stat = fs_1.default.statSync(guard);
599
+ }
600
+ catch {
601
+ res.status(404).json({ skipped: 'not-found' });
602
+ return;
603
+ }
604
+ if (!stat.isFile()) {
605
+ res.status(400).json({ skipped: 'not-file' });
606
+ return;
607
+ }
608
+ if (stat.size > MAX_FILE_BYTES) {
609
+ res.status(413).json({ skipped: 'too-large' });
610
+ return;
611
+ }
612
+ try {
613
+ const fd = fs_1.default.openSync(guard, 'r');
614
+ try {
615
+ const head = Buffer.alloc(Math.min(BINARY_PROBE_BYTES, stat.size));
616
+ fs_1.default.readSync(fd, head, 0, head.length, 0);
617
+ if (head.includes(0)) {
618
+ res.status(415).json({ skipped: 'binary' });
619
+ return;
620
+ }
621
+ }
622
+ finally {
623
+ fs_1.default.closeSync(fd);
624
+ }
625
+ }
626
+ catch {
627
+ res.status(500).json({ error: 'failed to inspect file' });
628
+ return;
629
+ }
630
+ const body = (req.body ?? {});
631
+ try {
632
+ // force: true — an explicit "Regenerate" click should re-summarise even if
633
+ // the content hash is unchanged (e.g. after an app language switch).
634
+ const result = await deps.fileSummaryManager.enqueue({
635
+ projectPath: deps.projectPath,
636
+ projectId: deps.projectId,
637
+ projectSlug: deps.projectId,
638
+ relPath: rel,
639
+ triggeredBy: { kind: 'user', id: 'manual', ticketId: null },
640
+ overrideBudget: body.overrideBudget === true,
641
+ force: true,
642
+ });
643
+ // Surface the enqueue outcome so the client's budget-override prompt is
644
+ // reachable. 200 (not 4xx) keeps res.ok true so the client reads `skipped`.
645
+ if (result === 'skipped:budget') {
646
+ res.status(200).json({ skipped: 'budget' });
647
+ return;
648
+ }
649
+ if (result === 'skipped:per-job-cap') {
650
+ res.status(200).json({ skipped: 'per-job-cap' });
651
+ return;
652
+ }
653
+ // TTL-dropped (queue saturated >5min) and not-found (file vanished between
654
+ // the stat above and the worker) must NOT masquerade as a 202 success —
655
+ // surface them so the client toasts "try again" instead of silently
656
+ // clearing the spinner with no summary.
657
+ if (result === 'skipped:ttl') {
658
+ res.status(200).json({ skipped: 'ttl' });
659
+ return;
660
+ }
661
+ if (result === 'skipped:not-found') {
662
+ res.status(200).json({ skipped: 'not-found' });
663
+ return;
664
+ }
665
+ if (result === 'failed') {
666
+ res.status(500).json({ error: 'summary generation failed' });
667
+ return;
668
+ }
669
+ res.status(202).json({ enqueued: true });
670
+ }
671
+ catch (err) {
672
+ console.error('[code-explorer-router] enqueue failed:', err);
673
+ res.status(500).json({ error: 'enqueue failed', message: err.message });
674
+ }
675
+ });
676
+ router.get('/provenance', (req, res) => {
677
+ const ticketId = parsePositiveInt(req.query.ticketId);
678
+ const jobId = parseNonEmptyString(req.query.jobId);
679
+ const relPath = parseNonEmptyString(req.query.path);
680
+ if (relPath) {
681
+ const guard = resolveSafePath(deps.projectPath, relPath);
682
+ if (!guard) {
683
+ res.status(400).json({ error: 'path traversal not allowed' });
684
+ return;
685
+ }
686
+ // Mirror the content endpoints: never leak even the metadata (which ticket/
687
+ // job touched it, when) of a denied/secret file.
688
+ if (isDeniedRelPath(relPath)) {
689
+ res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
690
+ return;
691
+ }
692
+ }
693
+ if (ticketId == null && !jobId && !relPath) {
694
+ res.status(400).json({ error: 'ticketId, jobId, or path query parameter is required' });
695
+ return;
696
+ }
697
+ if (req.query.ticketId != null && ticketId == null) {
698
+ res.status(400).json({ error: 'ticketId must be a positive integer' });
699
+ return;
700
+ }
701
+ const rows = ticketId != null && !jobId && !relPath
702
+ ? listByTicket(deps.db, deps.projectId, ticketId)
703
+ : listTouchedRows(deps.db, { ticketId, jobId, path: relPath ? normalizeRel(relPath) : relPath });
704
+ res.json(provenanceRowsToJson(rows));
705
+ });
706
+ router.get('/diff', (req, res) => {
707
+ const jobId = parseNonEmptyString(req.query.jobId);
708
+ const relPath = parseNonEmptyString(req.query.path);
709
+ if (!jobId || !relPath) {
710
+ res.status(400).json({ error: 'jobId and path query parameters are required' });
711
+ return;
712
+ }
713
+ const guard = resolveSafePath(deps.projectPath, relPath);
714
+ if (!guard) {
715
+ res.status(400).json({ error: 'path traversal not allowed' });
716
+ return;
717
+ }
718
+ const diff = (0, file_provenance_1.getProvenanceDiff)(deps.db, deps.projectId, jobId, normalizeRel(relPath));
719
+ if (!diff) {
720
+ res.status(404).json({ error: 'diff not available' });
721
+ return;
722
+ }
723
+ res.json(diff);
724
+ });
725
+ return router;
726
+ }
727
+ function resolveSafePath(projectPath, relPath) {
728
+ // Reject absolute paths and any path with explicit traversal segments before
729
+ // we ever hit the filesystem. resolve() can collapse `..` legally so we still
730
+ // verify the prefix below.
731
+ if (path_1.default.isAbsolute(relPath))
732
+ return null;
733
+ const resolved = path_1.default.resolve(projectPath, relPath);
734
+ const root = projectPath.endsWith(path_1.default.sep) ? projectPath : projectPath + path_1.default.sep;
735
+ if (resolved !== projectPath && !resolved.startsWith(root))
736
+ return null;
737
+ // Symlink hardening: the lexical check above is defeated by an in-tree symlink
738
+ // whose target escapes the project (e.g. `link -> /etc/passwd`). Verify the
739
+ // REAL path stays under the REAL project root. Walk up to the nearest existing
740
+ // ancestor (so not-yet-created paths — used by the not-found banner and the
741
+ // regenerate endpoint — still validate), realpath it, then re-append the
742
+ // missing suffix.
743
+ let realRoot;
744
+ try {
745
+ realRoot = fs_1.default.realpathSync.native(projectPath);
746
+ }
747
+ catch {
748
+ // Project root itself is uncanonicalisable — fail CLOSED. Returning the
749
+ // lexical path here would silently drop the symlink-escape hardening (the
750
+ // lexical check alone is defeatable by an in-tree symlink pointing outside
751
+ // the project). Reading any file is pointless if the root can't resolve.
752
+ return null;
753
+ }
754
+ const realRootWithSep = realRoot.endsWith(path_1.default.sep) ? realRoot : realRoot + path_1.default.sep;
755
+ let probe = resolved;
756
+ const suffix = [];
757
+ for (;;) {
758
+ try {
759
+ const realProbe = fs_1.default.realpathSync.native(probe);
760
+ const realFull = suffix.length > 0
761
+ ? path_1.default.join(realProbe, ...suffix.slice().reverse())
762
+ : realProbe;
763
+ if (realFull !== realRoot && !realFull.startsWith(realRootWithSep))
764
+ return null;
765
+ return resolved;
766
+ }
767
+ catch (err) {
768
+ if (err.code !== 'ENOENT')
769
+ return null;
770
+ const parent = path_1.default.dirname(probe);
771
+ if (parent === probe)
772
+ return null; // hit filesystem root without resolving
773
+ suffix.push(path_1.default.basename(probe));
774
+ probe = parent;
775
+ }
776
+ }
777
+ }
778
+ async function computeStaleness(abs, summary) {
779
+ if (!summary)
780
+ return false;
781
+ try {
782
+ const currentHash = await (0, file_summary_manager_1.computeFileHash)(abs);
783
+ return currentHash !== summary.fileHash;
784
+ }
785
+ catch {
786
+ return true;
787
+ }
788
+ }