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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/app-path-routes-manifest.json +9 -0
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/routes-manifest.json +62 -0
- package/app/apps/web/.next/server/app/_global-error/page.js +1 -1
- package/app/apps/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/agent/page/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/agent/page/build-manifest.json +17 -0
- package/app/apps/web/.next/server/app/agent/page/next-font-manifest.json +6 -0
- package/app/apps/web/.next/server/app/agent/page/react-loadable-manifest.json +1 -0
- package/app/apps/web/.next/server/app/agent/page/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/agent/page.js +18 -0
- package/app/apps/web/.next/server/app/agent/page.js.map +5 -0
- package/app/apps/web/.next/server/app/agent/page.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/agent/page_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js +7 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/events/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/messages/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js +7 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js +7 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/chats/[id]/stop/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route.js +7 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/chats/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/index/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/index/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/index/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/index/route.js +9 -0
- package/app/apps/web/.next/server/app/api/agent/index/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/index/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/index/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route.js +10 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/mcp/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route/app-paths-manifest.json +3 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route/build-manifest.json +9 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route/server-reference-manifest.json +4 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route.js +12 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route.js.map +5 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route.js.nft.json +1 -0
- package/app/apps/web/.next/server/app/api/agent/suggestions/route_client-reference-manifest.js +3 -0
- package/app/apps/web/.next/server/app/api/ask/route.js +3 -4
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +4 -4
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +4 -4
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js +2 -2
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +4 -4
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js +2 -2
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js +3 -3
- package/app/apps/web/.next/server/app/api/pdf/[key]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/projects/open-folder/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/credentials/route.js +5 -5
- package/app/apps/web/.next/server/app/api/settings/credentials/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js +3 -3
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team/autostart/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team/onboarding/start/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/team/sync-projects/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/workflow-agent/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js +2 -2
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js +2 -2
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page.js +2 -2
- package/app/apps/web/.next/server/app/print/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/print/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js +2 -2
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page.js +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/onboarding/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js +1 -1
- package/app/apps/web/.next/server/app/team/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/team/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app-paths-manifest.json +9 -0
- 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
- package/app/apps/web/.next/server/chunks/02dz_web__next-internal_server_app_api_agent_chats_[id]_stop_route_actions_036fon_.js +3 -0
- package/app/apps/web/.next/server/chunks/09og__next-internal_server_app_api_agent_chats_[id]_messages_route_actions_0ubwh4q.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__01~cgp8._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__02faosk._.js +30 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__02puo0u._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0367vbv._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__04g-b-m._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__06xto5z._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__096_llw._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__09cgmam._.js +28 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_5tf-2._.js +21 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gvw6dk._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0mx_9mb._.js +5 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0pirsh0._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0sbhe0s._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0tk8p3d._.js +28 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0u6pamj._.js +28 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__123tfcb._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__12jomao._.js +28 -0
- package/app/apps/web/.next/server/chunks/_132c.4p._.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_chats_[id]_route_actions_0b95y8h.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_chats_route_actions_07b4w1x.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_index_route_actions_03~ylvr.js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_mcp_route_actions_0-b30h..js +3 -0
- package/app/apps/web/.next/server/chunks/apps_web__next-internal_server_app_api_agent_suggestions_route_actions_0r_5s~k.js +3 -0
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0sr4wr_.js +13 -0
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0z_xi9k.js +16 -0
- package/app/apps/web/.next/server/chunks/{packages_0o6.gpk._.js → packages_entries_dist_0-0s5ui._.js} +2 -2
- package/app/apps/web/.next/server/chunks/packages_entries_dist_0~0d7ku._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +8 -8
- package/app/apps/web/.next/server/chunks/packages_parser_dist_06d9se1._.js +1 -1
- package/app/apps/web/.next/server/chunks/packages_parser_dist_10ihc3~._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0e.gxk3._.js → [root-of-the-server]__0.wm54e._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__04il6m2._.js → [root-of-the-server]__04qodwp._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0k1x-c9._.js → [root-of-the-server]__07~kk2~._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__03uoeiw._.js → [root-of-the-server]__0_78i1t._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0amklf0._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0nr2~y1._.js → [root-of-the-server]__0d5y90m._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__03svi~4._.js → [root-of-the-server]__0iv286q._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0j683am._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__0rvzdnx._.js → [root-of-the-server]__0kml6jt._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/{[root-of-the-server]__08l86e-._.js → [root-of-the-server]__0lmftab._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ns6ydi._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0vo.r~s._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~8.fgs._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0k8yioi._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{_13jdm1g._.js → _0~r5ica._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/apps_web__next-internal_server_app_agent_page_actions_0tuled1.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_day_[date]_day-view_tsx_0wjoe1u._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_app_sessions_[id]_session-view_tsx_0-9t5~.._.js +12 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0x014hn._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_agent-chat_tsx_0xbtx2n._.js +5 -0
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0n7_p1d._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_changelog_ts_0nb5_-s._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/{node_modules_0yrkyef._.js → node_modules_next_dist_0t-hj0x._.js} +2 -2
- package/app/apps/web/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0~g07z5.js +4 -0
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_0mx_509._.js +1 -1
- package/app/apps/web/.next/server/functions-config-manifest.json +8 -0
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/middleware-manifest.json +5 -5
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/04feoajklmrl3.js +3 -0
- package/app/apps/web/.next/static/chunks/07bzlfv97o1ig.js +10 -0
- package/app/apps/web/.next/static/chunks/0d~7p_ye7v7_5.css +1 -0
- package/app/apps/web/.next/static/chunks/0iiqpl6d_3i5~.js +1 -0
- package/app/apps/web/.next/static/chunks/0mry~475v0fa3.js +14 -0
- package/app/apps/web/.next/static/chunks/0qwgm81o9o~um.js +1 -0
- package/app/apps/web/.next/static/chunks/{0y3448zrint0k.js → 0~rycwo36m~c3.js} +1 -1
- package/app/apps/web/.next/static/chunks/12zmbd3mlcbn_.js +1 -0
- package/app/apps/web/.next/static/chunks/16bapkvp5.x37.js +1 -0
- package/app/apps/web/app/agent/page.tsx +12 -0
- package/app/apps/web/app/api/agent/chats/[id]/events/route.ts +86 -0
- package/app/apps/web/app/api/agent/chats/[id]/messages/route.ts +47 -0
- package/app/apps/web/app/api/agent/chats/[id]/route.ts +19 -0
- package/app/apps/web/app/api/agent/chats/[id]/stop/route.ts +10 -0
- package/app/apps/web/app/api/agent/chats/route.ts +26 -0
- package/app/apps/web/app/api/agent/index/route.ts +56 -0
- package/app/apps/web/app/api/agent/mcp/route.ts +31 -0
- package/app/apps/web/app/api/agent/suggestions/route.ts +150 -0
- package/app/apps/web/components/agent-chat.tsx +940 -0
- package/app/apps/web/components/sidebar.tsx +8 -21
- package/app/apps/web/lib/agent/chat-model.ts +125 -0
- package/app/apps/web/lib/agent/chat-store.test.ts +125 -0
- package/app/apps/web/lib/agent/chat-store.ts +59 -0
- package/app/apps/web/lib/agent/extract.test.ts +141 -0
- package/app/apps/web/lib/agent/extract.ts +75 -0
- package/app/apps/web/lib/agent/index-store.ts +206 -0
- package/app/apps/web/lib/agent/links.test.ts +27 -0
- package/app/apps/web/lib/agent/links.ts +10 -0
- package/app/apps/web/lib/agent/mcp.test.ts +83 -0
- package/app/apps/web/lib/agent/mcp.ts +85 -0
- package/app/apps/web/lib/agent/prompt.test.ts +38 -0
- package/app/apps/web/lib/agent/prompt.ts +44 -0
- package/app/apps/web/lib/agent/run.ts +426 -0
- package/app/apps/web/lib/agent/search.test.ts +111 -0
- package/app/apps/web/lib/agent/search.ts +144 -0
- package/app/apps/web/lib/agent/suggestions.test.ts +156 -0
- package/app/apps/web/lib/agent/suggestions.ts +167 -0
- package/app/apps/web/lib/agent/tools.test.ts +98 -0
- package/app/apps/web/lib/agent/tools.ts +176 -0
- package/app/apps/web/lib/agent/types.ts +50 -0
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/app/apps/web/vitest.config.ts +4 -0
- package/dist/daemon-worker.js +9 -4
- package/dist/index.js +19 -12
- package/menubar/FleetlensMenubar.app/Contents/Info.plist +2 -2
- package/menubar/FleetlensMenubar.app/Contents/MacOS/FleetlensMenubar +0 -0
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__03ewxxu._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__03rmbz6._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__05z0pcw._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0acikq.._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0b5r~-1._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jbc7tz._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ow1kn1._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0t7i-hy._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0~xt99s._.js +0 -3
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +0 -21
- package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0g-tomj._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0~.f_3z._.js +0 -12
- package/app/apps/web/.next/static/chunks/02xxpexzk3rf-.js +0 -1
- package/app/apps/web/.next/static/chunks/044mtrzh7d7va.js +0 -10
- package/app/apps/web/.next/static/chunks/05-k9oryi9wz1.js +0 -14
- package/app/apps/web/.next/static/chunks/05b0dn.r00hq1.js +0 -1
- package/app/apps/web/.next/static/chunks/0t2on1dhydyb2.css +0 -1
- package/app/apps/web/.next/static/chunks/0tliv91mhmrzn.js +0 -1
- /package/app/apps/web/.next/static/{5IC6kxw0JoPeCIPdnTpEp → bTww6lPxII4wde65VQDrF}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{5IC6kxw0JoPeCIPdnTpEp → bTww6lPxII4wde65VQDrF}/_clientMiddlewareManifest.js +0 -0
- /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
|
+
}
|