fleetlens 0.15.7 → 0.16.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 (303) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/app-path-routes-manifest.json +9 -0
  3. package/app/apps/web/.next/build-manifest.json +3 -3
  4. package/app/apps/web/.next/prerender-manifest.json +3 -3
  5. package/app/apps/web/.next/routes-manifest.json +62 -0
  6. package/app/apps/web/.next/server/app/_global-error/page.js +1 -1
  7. package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  13. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  14. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
  16. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/app/apps/web/.next/server/app/agent/page/app-paths-manifest.json +3 -0
  19. package/app/apps/web/.next/server/app/agent/page/build-manifest.json +17 -0
  20. package/app/apps/web/.next/server/app/agent/page/next-font-manifest.json +6 -0
  21. package/app/apps/web/.next/server/app/agent/page/react-loadable-manifest.json +1 -0
  22. package/app/apps/web/.next/server/app/agent/page/server-reference-manifest.json +4 -0
  23. package/app/apps/web/.next/server/app/agent/page.js +18 -0
  24. package/app/apps/web/.next/server/app/agent/page.js.map +5 -0
  25. package/app/apps/web/.next/server/app/agent/page.js.nft.json +1 -0
  26. package/app/apps/web/.next/server/app/agent/page_client-reference-manifest.js +3 -0
  27. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/app-paths-manifest.json +3 -0
  28. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/build-manifest.json +9 -0
  29. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/server-reference-manifest.json +4 -0
  30. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js +7 -0
  31. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js.map +5 -0
  32. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js.nft.json +1 -0
  33. package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route_client-reference-manifest.js +3 -0
  34. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/app-paths-manifest.json +3 -0
  35. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/build-manifest.json +9 -0
  36. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/server-reference-manifest.json +4 -0
  37. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js +9 -0
  38. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js.map +5 -0
  39. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js.nft.json +1 -0
  40. package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route_client-reference-manifest.js +3 -0
  41. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/app-paths-manifest.json +3 -0
  42. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/build-manifest.json +9 -0
  43. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/server-reference-manifest.json +4 -0
  44. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js +7 -0
  45. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js.map +5 -0
  46. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js.nft.json +1 -0
  47. package/app/apps/web/.next/server/app/api/agent/chats/[id]/route_client-reference-manifest.js +3 -0
  48. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/app-paths-manifest.json +3 -0
  49. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/build-manifest.json +9 -0
  50. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/server-reference-manifest.json +4 -0
  51. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js +7 -0
  52. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js.map +5 -0
  53. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js.nft.json +1 -0
  54. package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route_client-reference-manifest.js +3 -0
  55. package/app/apps/web/.next/server/app/api/agent/chats/route/app-paths-manifest.json +3 -0
  56. package/app/apps/web/.next/server/app/api/agent/chats/route/build-manifest.json +9 -0
  57. package/app/apps/web/.next/server/app/api/agent/chats/route/server-reference-manifest.json +4 -0
  58. package/app/apps/web/.next/server/app/api/agent/chats/route.js +7 -0
  59. package/app/apps/web/.next/server/app/api/agent/chats/route.js.map +5 -0
  60. package/app/apps/web/.next/server/app/api/agent/chats/route.js.nft.json +1 -0
  61. package/app/apps/web/.next/server/app/api/agent/chats/route_client-reference-manifest.js +3 -0
  62. package/app/apps/web/.next/server/app/api/agent/index/route/app-paths-manifest.json +3 -0
  63. package/app/apps/web/.next/server/app/api/agent/index/route/build-manifest.json +9 -0
  64. package/app/apps/web/.next/server/app/api/agent/index/route/server-reference-manifest.json +4 -0
  65. package/app/apps/web/.next/server/app/api/agent/index/route.js +9 -0
  66. package/app/apps/web/.next/server/app/api/agent/index/route.js.map +5 -0
  67. package/app/apps/web/.next/server/app/api/agent/index/route.js.nft.json +1 -0
  68. package/app/apps/web/.next/server/app/api/agent/index/route_client-reference-manifest.js +3 -0
  69. package/app/apps/web/.next/server/app/api/agent/mcp/route/app-paths-manifest.json +3 -0
  70. package/app/apps/web/.next/server/app/api/agent/mcp/route/build-manifest.json +9 -0
  71. package/app/apps/web/.next/server/app/api/agent/mcp/route/server-reference-manifest.json +4 -0
  72. package/app/apps/web/.next/server/app/api/agent/mcp/route.js +10 -0
  73. package/app/apps/web/.next/server/app/api/agent/mcp/route.js.map +5 -0
  74. package/app/apps/web/.next/server/app/api/agent/mcp/route.js.nft.json +1 -0
  75. package/app/apps/web/.next/server/app/api/agent/mcp/route_client-reference-manifest.js +3 -0
  76. package/app/apps/web/.next/server/app/api/agent/suggestions/route/app-paths-manifest.json +3 -0
  77. package/app/apps/web/.next/server/app/api/agent/suggestions/route/build-manifest.json +9 -0
  78. package/app/apps/web/.next/server/app/api/agent/suggestions/route/server-reference-manifest.json +4 -0
  79. package/app/apps/web/.next/server/app/api/agent/suggestions/route.js +12 -0
  80. package/app/apps/web/.next/server/app/api/agent/suggestions/route.js.map +5 -0
  81. package/app/apps/web/.next/server/app/api/agent/suggestions/route.js.nft.json +1 -0
  82. package/app/apps/web/.next/server/app/api/agent/suggestions/route_client-reference-manifest.js +3 -0
  83. package/app/apps/web/.next/server/app/api/ask/route.js +3 -4
  84. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  85. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +4 -4
  86. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  87. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +4 -4
  88. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  89. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +2 -2
  90. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  91. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +4 -4
  92. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  93. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +2 -2
  94. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  95. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  96. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  97. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +3 -3
  98. package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
  99. package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.nft.json +1 -1
  100. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  101. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  102. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  103. package/app/apps/web/.next/server/app/api/settings/credentials/route.js +5 -5
  104. package/app/apps/web/.next/server/app/api/settings/credentials/route.js.nft.json +1 -1
  105. package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
  106. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  107. package/app/apps/web/.next/server/app/api/team/autostart/route.js.nft.json +1 -1
  108. package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.nft.json +1 -1
  109. package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.nft.json +1 -1
  110. package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
  111. package/app/apps/web/.next/server/app/changelog/page.js +1 -1
  112. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  113. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  114. package/app/apps/web/.next/server/app/day/[date]/page.js +2 -2
  115. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  116. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  117. package/app/apps/web/.next/server/app/day/page.js +1 -1
  118. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  119. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  120. package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
  121. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  122. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  123. package/app/apps/web/.next/server/app/digest/page.js +1 -1
  124. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  125. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  126. package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
  127. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  128. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  129. package/app/apps/web/.next/server/app/insights/page.js +2 -2
  130. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  131. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  132. package/app/apps/web/.next/server/app/page.js +2 -2
  133. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  134. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  135. package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
  136. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  137. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  138. package/app/apps/web/.next/server/app/print/[key]/page.js +2 -2
  139. package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
  140. package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
  141. package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
  142. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  143. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  144. package/app/apps/web/.next/server/app/projects/page.js +1 -1
  145. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  146. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  147. package/app/apps/web/.next/server/app/runs/page.js +1 -1
  148. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  149. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  150. package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
  151. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  152. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  153. package/app/apps/web/.next/server/app/sessions/page.js +1 -1
  154. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  155. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  156. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  157. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  158. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  159. package/app/apps/web/.next/server/app/team/onboarding/page.js +1 -1
  160. package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -1
  161. package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +1 -1
  162. package/app/apps/web/.next/server/app/team/page.js +1 -1
  163. package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
  164. package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
  165. package/app/apps/web/.next/server/app/usage/page.js +1 -1
  166. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  167. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  168. package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
  169. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_agent_chats_[id]_events_route_actions_0fkgl_r.js +3 -0
  170. package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_agent_chats_[id]_stop_route_actions_036fon_.js +3 -0
  171. package/app/apps/web/.next/server/chunks/09og__next-internal_server_app_api_agent_chats_[id]_messages_route_actions_0ubwh4q.js +3 -0
  172. package/app/apps/web/.next/server/chunks/[root-of-the-server]__01~cgp8._.js +3 -0
  173. package/app/apps/web/.next/server/chunks/[root-of-the-server]__02faosk._.js +30 -0
  174. package/app/apps/web/.next/server/chunks/[root-of-the-server]__02puo0u._.js +3 -0
  175. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0367vbv._.js +3 -0
  176. package/app/apps/web/.next/server/chunks/[root-of-the-server]__04g-b-m._.js +9 -0
  177. package/app/apps/web/.next/server/chunks/[root-of-the-server]__06xto5z._.js +9 -0
  178. package/app/apps/web/.next/server/chunks/[root-of-the-server]__096_llw._.js +3 -0
  179. package/app/apps/web/.next/server/chunks/[root-of-the-server]__09cgmam._.js +28 -0
  180. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_5tf-2._.js +21 -0
  181. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gvw6dk._.js +3 -0
  182. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mx_9mb._.js +5 -0
  183. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0pirsh0._.js +9 -0
  184. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0sbhe0s._.js +3 -0
  185. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0tk8p3d._.js +28 -0
  186. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0u6pamj._.js +28 -0
  187. package/app/apps/web/.next/server/chunks/[root-of-the-server]__123tfcb._.js +3 -0
  188. package/app/apps/web/.next/server/chunks/[root-of-the-server]__12jomao._.js +28 -0
  189. package/app/apps/web/.next/server/chunks/_132c.4p._.js +3 -0
  190. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_chats_[id]_route_actions_0b95y8h.js +3 -0
  191. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_chats_route_actions_07b4w1x.js +3 -0
  192. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_index_route_actions_03~ylvr.js +3 -0
  193. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_mcp_route_actions_0-b30h..js +3 -0
  194. package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_suggestions_route_actions_0r_5s~k.js +3 -0
  195. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0sr4wr_.js +13 -0
  196. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0z_xi9k.js +16 -0
  197. package/app/apps/web/.next/server/chunks/{packages_0o6.gpk._.js → packages_entries_dist_0-0s5ui._.js} +2 -2
  198. package/app/apps/web/.next/server/chunks/packages_entries_dist_0~0d7ku._.js +3 -0
  199. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +8 -8
  200. package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
  201. package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
  202. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0e.gxk3._.js → [root-of-the-server]__0.wm54e._.js} +2 -2
  203. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__04il6m2._.js → [root-of-the-server]__04qodwp._.js} +2 -2
  204. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0k1x-c9._.js → [root-of-the-server]__07~kk2~._.js} +2 -2
  205. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__03uoeiw._.js → [root-of-the-server]__0_78i1t._.js} +2 -2
  206. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0amklf0._.js +3 -0
  207. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0nr2~y1._.js → [root-of-the-server]__0d5y90m._.js} +2 -2
  208. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__03svi~4._.js → [root-of-the-server]__0iv286q._.js} +2 -2
  209. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0j683am._.js +3 -0
  210. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0rvzdnx._.js → [root-of-the-server]__0kml6jt._.js} +2 -2
  211. package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08l86e-._.js → [root-of-the-server]__0lmftab._.js} +2 -2
  212. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ns6ydi._.js +3 -0
  213. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0vo.r~s._.js +3 -0
  214. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~8.fgs._.js +3 -0
  215. package/app/apps/web/.next/server/chunks/ssr/_0k8yioi._.js +1 -1
  216. package/app/apps/web/.next/server/chunks/ssr/{_13jdm1g._.js → _0~r5ica._.js} +2 -2
  217. package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_agent_page_actions_0tuled1.js +3 -0
  218. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
  219. package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_[id]_session-view_tsx_0-9t5~.._.js +12 -0
  220. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0x014hn._.js +1 -1
  221. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_agent-chat_tsx_0xbtx2n._.js +5 -0
  222. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
  223. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
  224. package/app/apps/web/.next/server/chunks/ssr/{node_modules_0yrkyef._.js → node_modules_next_dist_0t-hj0x._.js} +2 -2
  225. package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0~g07z5.js +4 -0
  226. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
  227. package/app/apps/web/.next/server/functions-config-manifest.json +8 -0
  228. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  229. package/app/apps/web/.next/server/middleware-manifest.json +5 -5
  230. package/app/apps/web/.next/server/pages/500.html +1 -1
  231. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  232. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  233. package/app/apps/web/.next/static/chunks/04feoajklmrl3.js +3 -0
  234. package/app/apps/web/.next/static/chunks/07bzlfv97o1ig.js +10 -0
  235. package/app/apps/web/.next/static/chunks/0d~7p_ye7v7_5.css +1 -0
  236. package/app/apps/web/.next/static/chunks/0iiqpl6d_3i5~.js +1 -0
  237. package/app/apps/web/.next/static/chunks/0mry~475v0fa3.js +14 -0
  238. package/app/apps/web/.next/static/chunks/0qwgm81o9o~um.js +1 -0
  239. package/app/apps/web/.next/static/chunks/{0y3448zrint0k.js → 0~rycwo36m~c3.js} +1 -1
  240. package/app/apps/web/.next/static/chunks/12zmbd3mlcbn_.js +1 -0
  241. package/app/apps/web/.next/static/chunks/16bapkvp5.x37.js +1 -0
  242. package/app/apps/web/app/agent/page.tsx +12 -0
  243. package/app/apps/web/app/api/agent/chats/[id]/events/route.ts +86 -0
  244. package/app/apps/web/app/api/agent/chats/[id]/messages/route.ts +47 -0
  245. package/app/apps/web/app/api/agent/chats/[id]/route.ts +19 -0
  246. package/app/apps/web/app/api/agent/chats/[id]/stop/route.ts +10 -0
  247. package/app/apps/web/app/api/agent/chats/route.ts +26 -0
  248. package/app/apps/web/app/api/agent/index/route.ts +56 -0
  249. package/app/apps/web/app/api/agent/mcp/route.ts +31 -0
  250. package/app/apps/web/app/api/agent/suggestions/route.ts +150 -0
  251. package/app/apps/web/components/agent-chat.tsx +940 -0
  252. package/app/apps/web/components/sidebar.tsx +8 -21
  253. package/app/apps/web/lib/agent/chat-model.ts +125 -0
  254. package/app/apps/web/lib/agent/chat-store.test.ts +125 -0
  255. package/app/apps/web/lib/agent/chat-store.ts +59 -0
  256. package/app/apps/web/lib/agent/extract.test.ts +141 -0
  257. package/app/apps/web/lib/agent/extract.ts +75 -0
  258. package/app/apps/web/lib/agent/index-store.ts +206 -0
  259. package/app/apps/web/lib/agent/links.test.ts +27 -0
  260. package/app/apps/web/lib/agent/links.ts +10 -0
  261. package/app/apps/web/lib/agent/mcp.test.ts +83 -0
  262. package/app/apps/web/lib/agent/mcp.ts +85 -0
  263. package/app/apps/web/lib/agent/prompt.test.ts +38 -0
  264. package/app/apps/web/lib/agent/prompt.ts +44 -0
  265. package/app/apps/web/lib/agent/run.ts +426 -0
  266. package/app/apps/web/lib/agent/search.test.ts +111 -0
  267. package/app/apps/web/lib/agent/search.ts +144 -0
  268. package/app/apps/web/lib/agent/suggestions.test.ts +156 -0
  269. package/app/apps/web/lib/agent/suggestions.ts +167 -0
  270. package/app/apps/web/lib/agent/tools.test.ts +98 -0
  271. package/app/apps/web/lib/agent/tools.ts +176 -0
  272. package/app/apps/web/lib/agent/types.ts +50 -0
  273. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  274. package/app/apps/web/package.json +1 -1
  275. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  276. package/app/apps/web/vitest.config.ts +4 -0
  277. package/dist/daemon-worker.js +9 -4
  278. package/dist/index.js +19 -12
  279. package/menubar/FleetlensMenubar.app/Contents/Info.plist +2 -2
  280. package/menubar/FleetlensMenubar.app/Contents/MacOS/FleetlensMenubar +0 -0
  281. package/package.json +1 -1
  282. package/app/apps/web/.next/server/chunks/[root-of-the-server]__03ewxxu._.js +0 -9
  283. package/app/apps/web/.next/server/chunks/[root-of-the-server]__03rmbz6._.js +0 -3
  284. package/app/apps/web/.next/server/chunks/[root-of-the-server]__05z0pcw._.js +0 -3
  285. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0acikq.._.js +0 -9
  286. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0b5r~-1._.js +0 -3
  287. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jbc7tz._.js +0 -3
  288. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ow1kn1._.js +0 -9
  289. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0t7i-hy._.js +0 -3
  290. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~xt99s._.js +0 -3
  291. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +0 -21
  292. package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +0 -3
  293. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0g-tomj._.js +0 -3
  294. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +0 -12
  295. package/app/apps/web/.next/static/chunks/02xxpexzk3rf-.js +0 -1
  296. package/app/apps/web/.next/static/chunks/044mtrzh7d7va.js +0 -10
  297. package/app/apps/web/.next/static/chunks/05-k9oryi9wz1.js +0 -14
  298. package/app/apps/web/.next/static/chunks/05b0dn.r00hq1.js +0 -1
  299. package/app/apps/web/.next/static/chunks/0t2on1dhydyb2.css +0 -1
  300. package/app/apps/web/.next/static/chunks/0tliv91mhmrzn.js +0 -1
  301. /package/app/apps/web/.next/static/{5IC6kxw0JoPeCIPdnTpEp → bTww6lPxII4wde65VQDrF}/_buildManifest.js +0 -0
  302. /package/app/apps/web/.next/static/{5IC6kxw0JoPeCIPdnTpEp → bTww6lPxII4wde65VQDrF}/_clientMiddlewareManifest.js +0 -0
  303. /package/app/apps/web/.next/static/{5IC6kxw0JoPeCIPdnTpEp → bTww6lPxII4wde65VQDrF}/_ssgManifest.js +0 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Detached agent runs. A run is a claude subprocess whose lifetime is
3
+ * NOT tied to any HTTP request: events stream into the chat file (so a
4
+ * closed tab loses nothing) and to any live SSE subscribers. The registry
5
+ * lives on globalThis — see the note in index-store.ts.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { randomUUID } from "node:crypto";
12
+ import { cclensPath } from "@claude-lens/parser/fs";
13
+ import { applyRunEvent, writeChat, type Chat, type RunEvent, type RunEventBody, type StoredMessage } from "./chat-store";
14
+ import { agentSystemPrompt, buildUserPrompt, type ChatMessage } from "./prompt";
15
+
16
+ export const MAX_CONCURRENT_RUNS = 3;
17
+ const HARD_TIMEOUT_MS = 5 * 60_000;
18
+ const MCP_CONNECT_ATTEMPTS = 3;
19
+ const MCP_RETRY_DELAY_MS = 750;
20
+ const DELTA_FLUSH_MS = 750;
21
+ const TOOL_INPUT_PREVIEW_MAX = 300;
22
+
23
+ type RunListener = (event: RunEvent) => void;
24
+
25
+ type ActiveRun = {
26
+ chatId: string;
27
+ events: RunEvent[];
28
+ subscribers: Set<RunListener>;
29
+ stoppedByUser: boolean;
30
+ /** Chat was deleted while running: never write its file again. */
31
+ discarded: boolean;
32
+ kill: () => void;
33
+ };
34
+
35
+ type Registry = { runs: Map<string, ActiveRun> };
36
+ const registry: Registry = ((globalThis as Record<string, unknown>).__fleetlensAgentRuns ??= {
37
+ runs: new Map<string, ActiveRun>(),
38
+ }) as Registry;
39
+
40
+ export function getRun(chatId: string): ActiveRun | undefined {
41
+ return registry.runs.get(chatId);
42
+ }
43
+
44
+ export function activeRunCount(): number {
45
+ return registry.runs.size;
46
+ }
47
+
48
+ export function killRun(chatId: string): void {
49
+ const run = registry.runs.get(chatId);
50
+ if (!run) return;
51
+ run.stoppedByUser = true;
52
+ run.kill();
53
+ }
54
+
55
+ /** Kill + forget: for chat deletion. The close handler must not re-persist
56
+ * the file (that would resurrect the deleted chat), and the concurrency
57
+ * slot frees immediately rather than when the subprocess finishes dying. */
58
+ export function discardRun(chatId: string): void {
59
+ const run = registry.runs.get(chatId);
60
+ if (!run) return;
61
+ run.stoppedByUser = true;
62
+ run.discarded = true;
63
+ registry.runs.delete(chatId);
64
+ run.kill();
65
+ }
66
+
67
+ /** A chat file that says "running" with no live run is a zombie (server
68
+ * restarted mid-run, or a crash beat the final flush). Repair on read. */
69
+ export function reconcileChat(chat: Chat): Chat {
70
+ if (chat.status !== "running" || registry.runs.has(chat.id)) return chat;
71
+ const repaired = applyRunEvent(chat, {
72
+ seq: chat.lastSeq + 1,
73
+ type: "error",
74
+ message: "run interrupted — the server restarted while it was working",
75
+ });
76
+ try {
77
+ writeChat(repaired);
78
+ } catch {
79
+ /* read-only repair still returned */
80
+ }
81
+ return repaired;
82
+ }
83
+
84
+ /** Same trace format as llm-runner.ts so agent runs show up on /runs
85
+ * and a misbehaving run (e.g. fabricated tool output) leaves a raw stream
86
+ * to diagnose. */
87
+ function newTrace(model: string, userPromptChars: number, systemPrompt: string, userPrompt: string) {
88
+ const runsDir = cclensPath("llm-runs");
89
+ const runId = `${new Date().toISOString().replace(/[:.]/g, "-")}_agent_chat_${randomUUID().slice(0, 8)}`;
90
+ const path = join(runsDir, `${runId}.jsonl`);
91
+ const write = (record: unknown, first = false) => {
92
+ try {
93
+ const line = JSON.stringify(record) + "\n";
94
+ if (first) {
95
+ mkdirSync(runsDir, { recursive: true });
96
+ writeFileSync(path, line);
97
+ } else {
98
+ appendFileSync(path, line);
99
+ }
100
+ } catch {
101
+ /* never block the run on a trace write */
102
+ }
103
+ };
104
+ write(
105
+ {
106
+ _meta: {
107
+ type: "start",
108
+ run_id: runId,
109
+ kind: "agent_chat",
110
+ model,
111
+ ts: new Date().toISOString(),
112
+ runtime: "detached",
113
+ user_prompt_chars: userPromptChars,
114
+ },
115
+ },
116
+ true,
117
+ );
118
+ write({ _meta: { type: "payload", run_id: runId, system_prompt: systemPrompt, user_prompt: userPrompt } });
119
+ return {
120
+ line: (raw: string) => {
121
+ try {
122
+ appendFileSync(path, raw + "\n");
123
+ } catch {}
124
+ },
125
+ end: (fields: Record<string, unknown>) =>
126
+ write({ _meta: { type: "end", run_id: runId, ts: new Date().toISOString(), ...fields } }),
127
+ };
128
+ }
129
+
130
+ function pruneInput(input: unknown): unknown {
131
+ const s = JSON.stringify(input) ?? "{}";
132
+ if (s.length <= TOOL_INPUT_PREVIEW_MAX) return input;
133
+ return { _truncated: s.slice(0, TOOL_INPUT_PREVIEW_MAX) + "…" };
134
+ }
135
+
136
+ /** Human line for a tool result: our own tools return JSON, so peek inside. */
137
+ function briefResult(text: string): string {
138
+ try {
139
+ const obj = JSON.parse(text) as Record<string, unknown>;
140
+ if (typeof obj.total_matches === "number") return `${obj.total_matches} matching sessions`;
141
+ if (Array.isArray(obj.sessions)) return `${(obj.total as number) ?? obj.sessions.length} sessions`;
142
+ if (Array.isArray(obj.projects)) return `${obj.projects.length} projects`;
143
+ } catch {
144
+ /* not JSON — fall through */
145
+ }
146
+ return `${text.length.toLocaleString()} chars`;
147
+ }
148
+
149
+ function toChatMessages(messages: StoredMessage[]): ChatMessage[] {
150
+ return messages
151
+ .map((m): ChatMessage => {
152
+ if (m.role === "user") return { role: "user", text: m.text };
153
+ const text = m.segments
154
+ .filter((s): s is Extract<typeof s, { kind: "text" }> => s.kind === "text")
155
+ .map((s) => s.text)
156
+ .join("");
157
+ return { role: "assistant", text };
158
+ })
159
+ .filter((m) => m.text.trim());
160
+ }
161
+
162
+ /** Start a detached run for `chat` (already persisted with the trailing
163
+ * user message + empty assistant message, status "running"). */
164
+ export function startRun(chat: Chat, opts: { model: string; mcpUrl: string }): void {
165
+ if (registry.runs.has(chat.id)) throw new Error("run already active for this chat");
166
+
167
+ let seq = chat.lastSeq;
168
+ let state = chat;
169
+ let lastFlushMs = 0;
170
+
171
+ const run: ActiveRun = {
172
+ chatId: chat.id,
173
+ events: [],
174
+ subscribers: new Set(),
175
+ stoppedByUser: false,
176
+ discarded: false,
177
+ kill: () => {}, // rebound to the live subprocess by each launch()
178
+ };
179
+ registry.runs.set(chat.id, run);
180
+
181
+ const persist = (force: boolean) => {
182
+ if (run.discarded) return; // deleted mid-run — don't resurrect the file
183
+ const now = Date.now();
184
+ if (!force && now - lastFlushMs < DELTA_FLUSH_MS) return;
185
+ lastFlushMs = now;
186
+ try {
187
+ writeChat(state);
188
+ } catch {
189
+ /* a failed flush is recovered by the next one */
190
+ }
191
+ };
192
+
193
+ const emit = (e: RunEventBody) => {
194
+ const event = { seq: ++seq, ...e } as RunEvent;
195
+ run.events.push(event);
196
+ state = applyRunEvent(state, event);
197
+ // Deltas flush on a timer; structural events flush immediately so the
198
+ // file never shows a tool call without its eventual result for long.
199
+ persist(event.type !== "delta");
200
+ for (const fn of run.subscribers) {
201
+ try {
202
+ fn(event);
203
+ } catch {
204
+ /* a broken subscriber must not stall the run */
205
+ }
206
+ }
207
+ };
208
+
209
+ const args = [
210
+ "-p",
211
+ "--output-format", "stream-json",
212
+ "--verbose",
213
+ "--include-partial-messages",
214
+ "--model", opts.model,
215
+ // Built-in tools off; the session-history MCP tools are the whole surface.
216
+ "--tools", "",
217
+ "--disable-slash-commands",
218
+ "--no-session-persistence",
219
+ "--setting-sources", "",
220
+ "--strict-mcp-config",
221
+ "--mcp-config", JSON.stringify({ mcpServers: { fleetlens: { type: "http", url: opts.mcpUrl } } }),
222
+ "--allowedTools",
223
+ "mcp__fleetlens__search_sessions",
224
+ "mcp__fleetlens__get_session",
225
+ "mcp__fleetlens__list_sessions",
226
+ "mcp__fleetlens__list_projects",
227
+ "--effort", "medium",
228
+ "--append-system-prompt", agentSystemPrompt(),
229
+ ];
230
+
231
+ // Neutral cwd: spawned from inside a repo, Claude Code would load that
232
+ // project's CLAUDE.md and auto-memory into context — the model then
233
+ // cites memory files as if they were linkable pages.
234
+ const runtimeDir = cclensPath("agent-runtime");
235
+ mkdirSync(runtimeDir, { recursive: true });
236
+ const userPrompt = buildUserPrompt(toChatMessages(chat.messages));
237
+
238
+ const killTimer = setTimeout(() => {
239
+ emit({ type: "error", message: `timed out after ${HARD_TIMEOUT_MS / 60000} minutes` });
240
+ run.kill();
241
+ }, HARD_TIMEOUT_MS);
242
+
243
+ const finish = () => {
244
+ clearTimeout(killTimer);
245
+ persist(true);
246
+ registry.runs.delete(chat.id);
247
+ };
248
+
249
+ const launch = (attempt: number) => {
250
+ if (run.stoppedByUser || state.status !== "running") {
251
+ if (run.stoppedByUser && state.status === "running") emit({ type: "error", message: "stopped by user" });
252
+ finish();
253
+ return;
254
+ }
255
+
256
+ const trace = newTrace(opts.model, userPrompt.length, agentSystemPrompt(), userPrompt);
257
+ const startMs = Date.now();
258
+ const proc = spawn("claude", args, {
259
+ stdio: ["pipe", "pipe", "pipe"],
260
+ env: { ...process.env },
261
+ cwd: runtimeDir,
262
+ });
263
+ run.kill = () => {
264
+ try {
265
+ proc.kill("SIGTERM");
266
+ } catch {}
267
+ };
268
+ proc.stdin.write(userPrompt);
269
+ proc.stdin.end();
270
+
271
+ /** This attempt was superseded by a retry — swallow everything else it produces. */
272
+ let abandoned = false;
273
+ let stderr = "";
274
+ let lineBuf = "";
275
+ let sawTerminal = false;
276
+ let usage: { input_tokens?: number; output_tokens?: number } | undefined;
277
+ /** Content-block indices that streamed text deltas for the in-flight
278
+ * assistant message. The complete-message frame only re-emits a text
279
+ * block if ITS deltas never arrived (CLI without partial messages, or
280
+ * a dropped delta line) — a per-message flag would drop a block whose
281
+ * deltas were lost whenever any other block streamed. */
282
+ let deltaBlocks = new Set<number>();
283
+ const toolNames = new Map<string, string>();
284
+
285
+ const handleLine = (t: string) => {
286
+ let obj: Record<string, unknown>;
287
+ try {
288
+ obj = JSON.parse(t) as Record<string, unknown>;
289
+ } catch {
290
+ return;
291
+ }
292
+ if (obj.type !== "stream_event") trace.line(t); // partial deltas would bloat the trace
293
+ if (obj.type === "system" && obj.subtype === "init") {
294
+ // The subprocess can race ahead of the HTTP MCP handshake: init
295
+ // reports the fleetlens server "pending" and the model runs with
296
+ // ZERO tools — it then "narrates" tool calls as text instead of
297
+ // making them. The tool list is fixed at init, so a pending server
298
+ // means a degraded run. Respawn (the handshake wins next time).
299
+ const servers = obj.mcp_servers as Array<{ name?: string; status?: string }> | undefined;
300
+ const fleetlens = servers?.find((s) => s.name === "fleetlens");
301
+ if (fleetlens && fleetlens.status !== "connected") {
302
+ abandoned = true;
303
+ trace.end({ runtime: "detached", elapsed_ms: Date.now() - startMs, mcp_status: fleetlens.status, retried: attempt < MCP_CONNECT_ATTEMPTS });
304
+ try {
305
+ proc.kill("SIGTERM");
306
+ } catch {}
307
+ if (attempt < MCP_CONNECT_ATTEMPTS) {
308
+ setTimeout(() => launch(attempt + 1), MCP_RETRY_DELAY_MS);
309
+ } else {
310
+ emit({ type: "error", message: `session tools failed to connect (mcp status: ${fleetlens.status}) — try again` });
311
+ finish();
312
+ }
313
+ }
314
+ return;
315
+ }
316
+ if (obj.type === "stream_event") {
317
+ const event = obj.event as Record<string, unknown> | undefined;
318
+ if (event?.type === "content_block_delta") {
319
+ const delta = event.delta as Record<string, unknown> | undefined;
320
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
321
+ if (typeof event.index === "number") deltaBlocks.add(event.index);
322
+ emit({ type: "delta", text: delta.text });
323
+ }
324
+ }
325
+ return;
326
+ }
327
+ if (obj.type === "assistant") {
328
+ const msg = obj.message as Record<string, unknown> | undefined;
329
+ const content = msg?.content as Array<Record<string, unknown>> | undefined;
330
+ if (Array.isArray(content)) {
331
+ content.forEach((block, i) => {
332
+ if (block.type === "text" && typeof block.text === "string" && !deltaBlocks.has(i)) {
333
+ emit({ type: "delta", text: block.text });
334
+ }
335
+ if (block.type === "tool_use" && typeof block.name === "string") {
336
+ const id = typeof block.id === "string" ? block.id : "";
337
+ const name = block.name.replace(/^mcp__fleetlens__/, "");
338
+ toolNames.set(id, name);
339
+ emit({ type: "tool", id, name, input: pruneInput(block.input) });
340
+ }
341
+ });
342
+ }
343
+ deltaBlocks = new Set();
344
+ return;
345
+ }
346
+ if (obj.type === "user") {
347
+ const msg = obj.message as Record<string, unknown> | undefined;
348
+ const content = msg?.content as Array<Record<string, unknown>> | undefined;
349
+ if (!Array.isArray(content)) return;
350
+ for (const block of content) {
351
+ if (block.type !== "tool_result") continue;
352
+ const id = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
353
+ const inner = block.content;
354
+ const text = Array.isArray(inner)
355
+ ? inner
356
+ .map((b) => (b && typeof b === "object" && "text" in b ? String((b as { text: unknown }).text) : ""))
357
+ .join("")
358
+ : typeof inner === "string"
359
+ ? inner
360
+ : "";
361
+ emit({
362
+ type: "tool_result",
363
+ id,
364
+ name: toolNames.get(id) ?? "tool",
365
+ brief: briefResult(text),
366
+ isError: block.is_error === true,
367
+ });
368
+ }
369
+ return;
370
+ }
371
+ if (obj.type === "result") {
372
+ sawTerminal = true;
373
+ usage = obj.usage as { input_tokens?: number; output_tokens?: number } | undefined;
374
+ const totalTokens =
375
+ typeof usage?.input_tokens === "number" && typeof usage?.output_tokens === "number"
376
+ ? usage.input_tokens + usage.output_tokens
377
+ : undefined;
378
+ emit({ type: "done", totalTokens });
379
+ }
380
+ };
381
+
382
+ // Streaming decoder: chunk.toString("utf8") would mangle a multi-byte
383
+ // character split across stdout reads, corrupting that JSON line.
384
+ const decoder = new TextDecoder("utf-8");
385
+ proc.stdout.on("data", (chunk: Buffer) => {
386
+ if (abandoned) return;
387
+ lineBuf += decoder.decode(chunk, { stream: true });
388
+ const lines = lineBuf.split("\n");
389
+ lineBuf = lines.pop() ?? "";
390
+ for (const line of lines) {
391
+ const t = line.trim();
392
+ if (t && !abandoned) handleLine(t);
393
+ }
394
+ });
395
+ proc.stderr.on("data", (c: Buffer) => {
396
+ // Only a 400-char tail is ever surfaced; cap so a chatty subprocess
397
+ // can't grow the buffer unbounded before the kill timer.
398
+ if (stderr.length < 4000) stderr += c.toString("utf8");
399
+ });
400
+ proc.on("close", (code) => {
401
+ if (abandoned) return; // the retry owns the run lifecycle now
402
+ if (!sawTerminal && state.status === "running") {
403
+ const message = run.stoppedByUser
404
+ ? "stopped by user"
405
+ : stderr.trim().slice(0, 400) || `claude exited with code ${code}`;
406
+ emit({ type: "error", message });
407
+ }
408
+ trace.end({
409
+ runtime: "detached",
410
+ elapsed_ms: Date.now() - startMs,
411
+ exit_code: code ?? 0,
412
+ input_tokens: usage?.input_tokens,
413
+ output_tokens: usage?.output_tokens,
414
+ });
415
+ finish();
416
+ });
417
+ proc.on("error", (err) => {
418
+ if (abandoned) return;
419
+ emit({ type: "error", message: `Failed to spawn claude: ${err.message}` });
420
+ trace.end({ runtime: "detached", elapsed_ms: Date.now() - startMs, exit_code: -1, error: err.message });
421
+ finish();
422
+ });
423
+ };
424
+
425
+ launch(1);
426
+ }
@@ -0,0 +1,111 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { IndexDoc } from "./types";
3
+ import { searchDocs } from "./search";
4
+
5
+ function doc(partial: Partial<IndexDoc> & { sessionId: string }): IndexDoc {
6
+ return {
7
+ version: 1,
8
+ agent: "claude-code",
9
+ project: "/Users/me/Repo/foo",
10
+ day: "2026-07-01",
11
+ title: "untitled",
12
+ chunks: [],
13
+ mtimeMs: 0,
14
+ sizeBytes: 0,
15
+ ...partial,
16
+ };
17
+ }
18
+
19
+ const DOCS: IndexDoc[] = [
20
+ doc({
21
+ sessionId: "s1",
22
+ day: "2026-07-08",
23
+ title: "fix usage daemon crash",
24
+ chunks: [
25
+ { role: "user", text: "the usage daemon keeps crashing on boot", idx: 0 },
26
+ { role: "agent", text: "The crash came from a stale pid file in ~/.cclens", idx: 1 },
27
+ ],
28
+ }),
29
+ doc({
30
+ sessionId: "s2",
31
+ day: "2026-06-01",
32
+ project: "/Users/me/Repo/bar",
33
+ agent: "codex",
34
+ title: "add usage chart",
35
+ chunks: [{ role: "agent", text: "Added the usage chart with recharts", idx: 0 }],
36
+ }),
37
+ doc({
38
+ sessionId: "s3",
39
+ day: "2026-07-09",
40
+ title: "refactor timeline",
41
+ chunks: [{ role: "user", text: "please refactor the timeline page", idx: 0 }],
42
+ }),
43
+ ];
44
+
45
+ describe("searchDocs", () => {
46
+ test("matches terms case-insensitively, ranking full-term matches first", () => {
47
+ const { hits, total } = searchDocs(DOCS, "USAGE daemon");
48
+ // s1 matches both terms, s2 matches only "usage" — ranked-OR semantics.
49
+ expect(total).toBe(2);
50
+ expect(hits[0]!.sessionId).toBe("s1");
51
+ expect(hits[0]!.matchedTerms.sort()).toEqual(["daemon", "usage"]);
52
+ });
53
+
54
+ test("docs matching all terms rank above partial matches", () => {
55
+ const { hits } = searchDocs(DOCS, "usage daemon chart");
56
+ // s1 matches usage+daemon (2 terms), s2 matches usage+chart (2 terms) — tie on
57
+ // count; but neither matches all 3. Both must appear, full-match-first ordering
58
+ // is covered by the score, so just assert both are present.
59
+ expect(hits.map((h) => h.sessionId).sort()).toEqual(["s1", "s2"]);
60
+ });
61
+
62
+ test("returns empty for no matches and blank queries", () => {
63
+ expect(searchDocs(DOCS, "zzzznope").hits).toEqual([]);
64
+ expect(searchDocs(DOCS, " ").hits).toEqual([]);
65
+ });
66
+
67
+ test("quoted phrases require exact substring match", () => {
68
+ const { hits } = searchDocs(DOCS, '"stale pid file"');
69
+ expect(hits.map((h) => h.sessionId)).toEqual(["s1"]);
70
+ expect(searchDocs(DOCS, '"stale file pid"').hits).toEqual([]);
71
+ });
72
+
73
+ test("filters by project, agent, and day bounds", () => {
74
+ expect(searchDocs(DOCS, "usage", { filters: { agent: "codex" } }).hits.map((h) => h.sessionId)).toEqual(["s2"]);
75
+ expect(
76
+ searchDocs(DOCS, "usage", { filters: { project: "/Users/me/Repo/foo" } }).hits.map((h) => h.sessionId),
77
+ ).toEqual(["s1"]);
78
+ expect(searchDocs(DOCS, "usage", { filters: { after: "2026-07-01" } }).hits.map((h) => h.sessionId)).toEqual(["s1"]);
79
+ expect(searchDocs(DOCS, "usage", { filters: { before: "2026-06-30" } }).hits.map((h) => h.sessionId)).toEqual(["s2"]);
80
+ });
81
+
82
+ test("produces snippets around the match with role attribution", () => {
83
+ const { hits } = searchDocs(DOCS, "pid");
84
+ const snip = hits[0]!.snippets[0]!;
85
+ expect(snip.role).toBe("agent");
86
+ expect(snip.text).toContain("stale pid file");
87
+ });
88
+
89
+ test("newer sessions outrank older ones at equal relevance", () => {
90
+ const twin = (id: string, day: string) =>
91
+ doc({ sessionId: id, day, title: "same", chunks: [{ role: "user", text: "identical content here", idx: 0 }] });
92
+ const { hits } = searchDocs([twin("old", "2025-01-01"), twin("new", "2026-07-01")], "identical");
93
+ expect(hits.map((h) => h.sessionId)).toEqual(["new", "old"]);
94
+ });
95
+
96
+ test("respects limit but reports full total", () => {
97
+ const many = Array.from({ length: 30 }, (_, i) =>
98
+ doc({ sessionId: `m${i}`, chunks: [{ role: "user", text: "needle in haystack", idx: 0 }] }),
99
+ );
100
+ const { hits, total } = searchDocs(many, "needle", { limit: 5 });
101
+ expect(hits).toHaveLength(5);
102
+ expect(total).toBe(30);
103
+ });
104
+
105
+ test("title matches outweigh body matches", () => {
106
+ const titled = doc({ sessionId: "titled", title: "daemon work", chunks: [{ role: "agent", text: "unrelated", idx: 0 }] });
107
+ const body = doc({ sessionId: "body", title: "misc", chunks: [{ role: "agent", text: "daemon mentioned once", idx: 0 }] });
108
+ const { hits } = searchDocs([body, titled], "daemon");
109
+ expect(hits[0]!.sessionId).toBe("titled");
110
+ });
111
+ });
@@ -0,0 +1,144 @@
1
+ import type { IndexDoc, SearchFilters, SearchHit, SearchSnippet } from "./types";
2
+
3
+ const SNIPPET_RADIUS = 120;
4
+ const MAX_SNIPPETS = 3;
5
+ const ROLE_WEIGHT: Record<string, number> = { user: 2, summary: 3, agent: 1 };
6
+ const TITLE_WEIGHT = 4;
7
+ /** Recency half-life: a hit ages to half its score every 60 days. */
8
+ const HALF_LIFE_MS = 60 * 86_400_000;
9
+
10
+ type ParsedQuery = { terms: string[]; phrases: string[] };
11
+
12
+ export function parseQuery(q: string): ParsedQuery {
13
+ const phrases: string[] = [];
14
+ const rest = q.replace(/"([^"]+)"/g, (_, p: string) => {
15
+ phrases.push(p.toLowerCase());
16
+ return " ";
17
+ });
18
+ const terms = Array.from(
19
+ new Set(
20
+ rest
21
+ .toLowerCase()
22
+ .split(/[^\p{L}\p{N}_./-]+/u)
23
+ .filter((t) => t.length >= 2),
24
+ ),
25
+ );
26
+ return { terms, phrases };
27
+ }
28
+
29
+ function passesFilters(doc: IndexDoc, f?: SearchFilters): boolean {
30
+ if (!f) return true;
31
+ if (f.project && doc.project !== f.project) return false;
32
+ if (f.agent && doc.agent !== f.agent) return false;
33
+ if (f.after && doc.day < f.after) return false;
34
+ if (f.before && doc.day > f.before) return false;
35
+ return true;
36
+ }
37
+
38
+ function countOccurrences(haystack: string, needle: string): number {
39
+ let count = 0;
40
+ let pos = haystack.indexOf(needle);
41
+ while (pos !== -1) {
42
+ count++;
43
+ pos = haystack.indexOf(needle, pos + needle.length);
44
+ }
45
+ return count;
46
+ }
47
+
48
+ function snippetAround(text: string, matchPos: number, matchLen: number): string {
49
+ const start = Math.max(0, matchPos - SNIPPET_RADIUS);
50
+ const end = Math.min(text.length, matchPos + matchLen + SNIPPET_RADIUS);
51
+ const core = text.slice(start, end).replace(/\s+/g, " ").trim();
52
+ return `${start > 0 ? "…" : ""}${core}${end < text.length ? "…" : ""}`;
53
+ }
54
+
55
+ export function searchDocs(
56
+ docs: IndexDoc[],
57
+ query: string,
58
+ opts: { filters?: SearchFilters; limit?: number } = {},
59
+ ): { hits: SearchHit[]; total: number } {
60
+ const { terms, phrases } = parseQuery(query);
61
+ if (terms.length === 0 && phrases.length === 0) return { hits: [], total: 0 };
62
+
63
+ const nowMs = Date.now();
64
+ const scored: SearchHit[] = [];
65
+
66
+ for (const doc of docs) {
67
+ if (!passesFilters(doc, opts.filters)) continue;
68
+
69
+ const titleLower = doc.title.toLowerCase();
70
+ const matched = new Set<string>();
71
+ let score = 0;
72
+ const snippets: SearchSnippet[] = [];
73
+
74
+ // Phrases are hard requirements.
75
+ let phrasesOk = true;
76
+ for (const phrase of phrases) {
77
+ let found = false;
78
+ if (titleLower.includes(phrase)) {
79
+ found = true;
80
+ score += TITLE_WEIGHT * 2;
81
+ }
82
+ for (const chunk of doc.chunks) {
83
+ const lower = chunk.text.toLowerCase();
84
+ const pos = lower.indexOf(phrase);
85
+ if (pos === -1) continue;
86
+ found = true;
87
+ score += (ROLE_WEIGHT[chunk.role] ?? 1) * 2;
88
+ if (snippets.length < MAX_SNIPPETS) {
89
+ snippets.push({ role: chunk.role, text: snippetAround(chunk.text, pos, phrase.length) });
90
+ }
91
+ }
92
+ if (!found) {
93
+ phrasesOk = false;
94
+ break;
95
+ }
96
+ matched.add(`"${phrase}"`);
97
+ }
98
+ if (!phrasesOk) continue;
99
+
100
+ for (const term of terms) {
101
+ let termHits = 0;
102
+ if (titleLower.includes(term)) {
103
+ termHits += TITLE_WEIGHT;
104
+ }
105
+ for (const chunk of doc.chunks) {
106
+ const lower = chunk.text.toLowerCase();
107
+ const n = countOccurrences(lower, term);
108
+ if (n === 0) continue;
109
+ // log-dampen repeats so one chatty session doesn't dominate on frequency
110
+ termHits += (ROLE_WEIGHT[chunk.role] ?? 1) * (1 + Math.log2(n));
111
+ if (termHits > 0 && snippets.length < MAX_SNIPPETS) {
112
+ const pos = lower.indexOf(term);
113
+ snippets.push({ role: chunk.role, text: snippetAround(chunk.text, pos, term.length) });
114
+ }
115
+ }
116
+ if (termHits > 0) {
117
+ matched.add(term);
118
+ score += termHits;
119
+ }
120
+ }
121
+
122
+ if (matched.size === 0) continue;
123
+
124
+ const docMs = doc.startIso ? Date.parse(doc.startIso) : NaN;
125
+ const ageMs = Number.isNaN(docMs) ? HALF_LIFE_MS : Math.max(0, nowMs - docMs);
126
+ const recency = Math.pow(0.5, ageMs / HALF_LIFE_MS);
127
+ // Full-term coverage dominates, then relevance × recency.
128
+ const allTerms = matched.size === terms.length + phrases.length ? 1 : 0;
129
+ scored.push({
130
+ sessionId: doc.sessionId,
131
+ agent: doc.agent,
132
+ project: doc.project,
133
+ day: doc.day,
134
+ title: doc.title,
135
+ score: allTerms * 1_000_000 + score * (0.25 + 0.75 * recency),
136
+ matchedTerms: Array.from(matched),
137
+ snippets: snippets.slice(0, MAX_SNIPPETS),
138
+ });
139
+ }
140
+
141
+ scored.sort((a, b) => b.score - a.score || b.day.localeCompare(a.day));
142
+ const limit = opts.limit ?? 10;
143
+ return { hits: scored.slice(0, limit), total: scored.length };
144
+ }