@yancyyu/openhermit 1.6.38 → 1.6.40

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 (243) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CemDOX-3.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-hPY770Db.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-BHHrJT1i.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CWErBtke.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-C_o2_Uv8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-DUW0LI3t.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-CWbCE9hQ.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-BjLadrfV.js} +1 -1
  9. package/dist-renderer/assets/channel-DyP9YlCF.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-CPnvjZl9.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-OlL47yXQ.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-DTasjbm8.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-C5_Xaqkk.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-NdoM4DMR.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-C8Fybejy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-E98TYFXJ.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-h4lFgkIq.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-BqffFTae.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BqffFTae.js +1 -0
  20. package/dist-renderer/assets/clone-MPcKWs2O.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-DtQ7fkrs.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-CN-nL_z4.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DVJtqmm-.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-DlxHxyXh.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-7dpzO6x6.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-GP1TqsHi.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-C7ZLETuH.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-CvPB68dH.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5yOm3w7.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-smeyY1YZ.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BJx8XvG1.js} +1 -1
  32. package/dist-renderer/assets/{index-C_okzZXP.js → index-CQaXUAua.js} +1 -1
  33. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-CajRpxO2.js} +1 -1
  34. package/dist-renderer/assets/{index-V7dAKPqd.js → index-ChG4rE-E.js} +587 -705
  35. package/dist-renderer/assets/index-DUd0uw9C.css +32 -0
  36. package/dist-renderer/assets/{index-CzWxVCRL.js → index-IhmXZWqf.js} +1 -1
  37. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-x_JkoDRH.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-D-hWRQGY.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-Bb6W8rUG.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-CnHdUX0q.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-pqss_zkI.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-B1mFITNh.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-DTD9q7-D.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-Df3mhrn7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-B1FZ09vH.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-aEO78thZ.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-6Ui--jp-.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-DF4Q1cAM.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-BqA2BI8C.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Cs2ZtUD2.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DoOkw_A8.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DUe26QdD.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-BKCnj5Xn.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +2 -1
  58. package/src/main/server.ts +873 -221
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  62. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  63. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  64. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  65. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  66. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  67. package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
  68. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  69. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  70. package/src/main/utils/teamProjectResolution.ts +15 -0
  71. package/src/renderer/App.tsx +8 -4
  72. package/src/renderer/api/httpClient.ts +68 -18
  73. package/src/renderer/api/providers.ts +23 -2
  74. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  75. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  76. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  87. package/src/renderer/components/chat/ChatHistoryItem.tsx +1 -1
  88. package/src/renderer/components/chat/items/SubagentItem.tsx +2 -2
  89. package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +2 -2
  90. package/src/renderer/components/common/ErrorBoundary.tsx +1 -1
  91. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  92. package/src/renderer/components/dashboard/CliStatusBanner.tsx +7 -7
  93. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  94. package/src/renderer/components/extensions/ExtensionStoreView.tsx +7 -126
  95. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  96. package/src/renderer/components/extensions/common/ExtensionToast.tsx +3 -3
  97. package/src/renderer/components/extensions/common/SourceBadge.tsx +1 -1
  98. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  99. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  100. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  101. package/src/renderer/components/extensions/plugins/CategoryChips.tsx +1 -1
  102. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  103. package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +2 -2
  104. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  105. package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +1 -1
  106. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  107. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  108. package/src/renderer/components/layout/PaneResizeHandle.tsx +2 -2
  109. package/src/renderer/components/layout/Sidebar.tsx +13 -56
  110. package/src/renderer/components/layout/SortableTab.tsx +22 -33
  111. package/src/renderer/components/layout/TabBar.tsx +1 -1
  112. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  113. package/src/renderer/components/report/sections/CostSection.tsx +2 -2
  114. package/src/renderer/components/report/sections/InsightsSection.tsx +1 -1
  115. package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +2 -2
  116. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  117. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  118. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  119. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  120. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  121. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  122. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  123. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  124. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  125. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  126. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  127. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  128. package/src/renderer/components/search/CommandPalette.tsx +4 -4
  129. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  130. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  131. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  132. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  133. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  134. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  135. package/src/renderer/components/settings/sections/CliStatusSection.tsx +72 -113
  136. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  137. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  138. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  139. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  140. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  141. package/src/renderer/components/settings/sections/index.ts +0 -1
  142. package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +1 -1
  143. package/src/renderer/components/sidebar/SessionItem.tsx +3 -3
  144. package/src/renderer/components/sidebar/SidebarSessions.tsx +184 -6
  145. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  146. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +40 -5
  147. package/src/renderer/components/splash/splashScene.ts +121 -929
  148. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  149. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  150. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  151. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  152. package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +1 -1
  153. package/src/renderer/components/team/ClaudeLogsPanel.tsx +1 -1
  154. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  155. package/src/renderer/components/team/ProcessesSection.tsx +2 -2
  156. package/src/renderer/components/team/TaskTooltip.tsx +2 -2
  157. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  158. package/src/renderer/components/team/TeamListFilterPopover.tsx +1 -1
  159. package/src/renderer/components/team/TeamListView.tsx +109 -124
  160. package/src/renderer/components/team/TeamSessionsSection.tsx +6 -6
  161. package/src/renderer/components/team/UnreadCommentsBadge.tsx +1 -1
  162. package/src/renderer/components/team/activity/ActivityItem.tsx +9 -9
  163. package/src/renderer/components/team/activity/ActivityTimeline.tsx +5 -5
  164. package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +3 -3
  165. package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +4 -4
  166. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +4 -4
  167. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  168. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  169. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  170. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +18 -16
  171. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  172. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +8 -1
  173. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +5 -5
  174. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  175. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +6 -6
  176. package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +6 -6
  177. package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +1 -1
  178. package/src/renderer/components/team/dialogs/TaskAttachments.tsx +1 -1
  179. package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +6 -6
  180. package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +4 -4
  181. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +3 -3
  182. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  183. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  184. package/src/renderer/components/team/editor/EditorFileTree.tsx +4 -4
  185. package/src/renderer/components/team/editor/EditorSearchPanel.tsx +1 -1
  186. package/src/renderer/components/team/editor/MarkdownSplitView.tsx +1 -1
  187. package/src/renderer/components/team/editor/NewFileDialog.tsx +1 -1
  188. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +1 -1
  189. package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +1 -1
  190. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  191. package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +4 -4
  192. package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +1 -1
  193. package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +5 -5
  194. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +4 -4
  195. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  196. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  197. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  198. package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
  199. package/src/renderer/components/team/members/MemberStatsTab.tsx +2 -2
  200. package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +1 -1
  201. package/src/renderer/components/team/messages/MessageComposer.tsx +10 -112
  202. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +1 -1
  203. package/src/renderer/components/team/messages/MessagesPanel.tsx +136 -119
  204. package/src/renderer/components/team/review/ChangeReviewDialog.tsx +1 -1
  205. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +3 -3
  206. package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +4 -4
  207. package/src/renderer/components/team/tasks/TaskRow.tsx +1 -1
  208. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  209. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  210. package/src/renderer/components/team/tools/SkillChip.tsx +3 -3
  211. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  212. package/src/renderer/components/ui/MemberSelect.tsx +2 -2
  213. package/src/renderer/components/ui/MentionSuggestionList.tsx +2 -2
  214. package/src/renderer/components/ui/MentionableTextarea.tsx +3 -3
  215. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  216. package/src/renderer/index.css +56 -39
  217. package/src/renderer/index.html +17 -50
  218. package/src/renderer/store/index.ts +2 -1
  219. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  220. package/src/renderer/store/slices/teamSlice.ts +45 -168
  221. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  222. package/src/renderer/utils/memberHelpers.ts +5 -17
  223. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  224. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  225. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  226. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  227. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  228. package/src/shared/types/api.ts +121 -2
  229. package/src/shared/types/ccConnect.ts +2 -0
  230. package/src/shared/types/index.ts +3 -0
  231. package/src/shared/types/systemManager.ts +49 -0
  232. package/src/shared/types/team.ts +29 -0
  233. package/src/shared/types/terminal.ts +4 -2
  234. package/src/shared/utils/extensionNormalizers.ts +15 -8
  235. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  236. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  237. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  238. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  239. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  240. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  241. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  242. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  243. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -0,0 +1,1101 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { createInterface } from 'node:readline';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+
7
+ import type { CcConnectClient } from '@main/services/ccConnect/CcConnectClient';
8
+ import { getProjectsBasePath } from '@main/utils/pathDecoder';
9
+ import type {
10
+ ConversationTelemetryExportFormat,
11
+ ConversationTelemetryQuery,
12
+ ConversationTelemetryResponse,
13
+ ConversationTelemetryRow,
14
+ ConversationTelemetryExportResponse,
15
+ ConversationTelemetryMessage,
16
+ } from '@shared/types/api';
17
+ import type { TeamManifest } from '../teams-mvp/TeamWorkspaceService';
18
+
19
+ interface ClaudeSessionSummary {
20
+ sessionId: string;
21
+ relPath: string;
22
+ filePath: string;
23
+ projectPath?: string;
24
+ startTime?: string;
25
+ endTime?: string;
26
+ messageCount: number;
27
+ userMessageCount: number;
28
+ assistantMessageCount: number;
29
+ toolResultCount: number;
30
+ inputTokens: number;
31
+ outputTokens: number;
32
+ cacheReadTokens: number;
33
+ cacheCreationTokens: number;
34
+ assistantTurnsWithUsage: number;
35
+ models: Record<string, number>;
36
+ toolCalls: Record<string, number>;
37
+ messages: ConversationTelemetryMessage[];
38
+ }
39
+
40
+ interface CachedClaudeSession {
41
+ size: number;
42
+ mtimeMs: number;
43
+ parsed: ClaudeSessionSummary;
44
+ }
45
+
46
+ interface ConversationIdentityRecord {
47
+ teamName: string;
48
+ projectName: string;
49
+ platform: string;
50
+ sessionKey: string;
51
+ ccSessionId?: string;
52
+ userId?: string;
53
+ chatId?: string;
54
+ userName?: string;
55
+ chatName?: string;
56
+ firstSeenAt: string;
57
+ lastSeenAt: string;
58
+ source: 'cc-session-name';
59
+ }
60
+
61
+ interface ConversationTelemetryServiceOptions {
62
+ cc: CcConnectClient;
63
+ listTeams: () => Promise<TeamManifest[]>;
64
+ readTeamManifest: (teamName: string) => Promise<TeamManifest>;
65
+ }
66
+
67
+ interface ConversationProject {
68
+ slug: string;
69
+ displayName: string;
70
+ bindProject: string;
71
+ }
72
+
73
+ const DEFAULT_LIMIT = 50;
74
+ const MAX_LIMIT = 100_000;
75
+
76
+ function telemetryRoot(): string {
77
+ return path.join(process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit'), 'telemetry');
78
+ }
79
+
80
+ function identityStorePath(): string {
81
+ return path.join(telemetryRoot(), 'conversation-identities.json');
82
+ }
83
+
84
+ function toNumber(value: unknown): number {
85
+ const n = Number(value ?? 0);
86
+ return Number.isFinite(n) ? n : 0;
87
+ }
88
+
89
+ function normalizeOptional(value: unknown): string | undefined {
90
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
91
+ }
92
+
93
+ function parseSessionIdentity(sessionKey: string): {
94
+ platform?: string;
95
+ chatId?: string;
96
+ userId?: string;
97
+ } {
98
+ const [platform, ...ids] = sessionKey.split(':').filter(Boolean);
99
+ const chatId =
100
+ ids.find((id) => /^(oc|chat|group|room)_/i.test(id)) ?? (ids.length >= 2 ? ids[0] : undefined);
101
+ const userId =
102
+ ids.find((id) => /^(ou|on|union|user|u)_/i.test(id)) ?? (ids.length >= 2 ? ids[1] : undefined);
103
+ return { platform: normalizeOptional(platform), chatId, userId };
104
+ }
105
+
106
+ function looksLikeChannelId(value: string | undefined): boolean {
107
+ return !!value && /^[A-Za-z]+_[A-Za-z0-9_-]+$/.test(value);
108
+ }
109
+
110
+ function formatIdentityFallback(
111
+ platform: string | undefined,
112
+ type: 'conversation' | 'group' | 'person',
113
+ id: string
114
+ ): string {
115
+ const prefix = platform ? `${platform} ` : '';
116
+ const label = type === 'group' ? '未解析群聊' : type === 'person' ? '未解析用户' : '未解析会话';
117
+ return `${prefix}${label} ${shortId(id)}`;
118
+ }
119
+
120
+ function shortId(value: string): string {
121
+ if (value.length <= 14) return value;
122
+ return `${value.slice(0, 6)}…${value.slice(-6)}`;
123
+ }
124
+
125
+ function extractText(content: unknown): string {
126
+ if (typeof content === 'string') return content;
127
+ if (!Array.isArray(content)) return '';
128
+
129
+ const parts: string[] = [];
130
+ for (const block of content) {
131
+ if (!block || typeof block !== 'object') continue;
132
+ const b = block as Record<string, unknown>;
133
+ if (b.type === 'text' && typeof b.text === 'string') {
134
+ parts.push(b.text);
135
+ } else if (b.type === 'thinking' && typeof b.thinking === 'string') {
136
+ parts.push('[Thinking omitted]');
137
+ } else if (b.type === 'tool_use' && typeof b.name === 'string') {
138
+ parts.push(`[Tool: ${b.name}]`);
139
+ } else if (b.type === 'tool_result') {
140
+ const nested = b.content;
141
+ if (typeof nested === 'string') parts.push(nested);
142
+ else if (Array.isArray(nested)) parts.push(extractText(nested));
143
+ } else if (b.type === 'image') {
144
+ parts.push('[Image]');
145
+ }
146
+ }
147
+ return parts.filter(Boolean).join('\n');
148
+ }
149
+
150
+ function countToolCalls(content: unknown, counts: Record<string, number>): void {
151
+ if (!Array.isArray(content)) return;
152
+ for (const block of content) {
153
+ if (!block || typeof block !== 'object') continue;
154
+ const b = block as Record<string, unknown>;
155
+ if (b.type === 'tool_use' && typeof b.name === 'string') {
156
+ counts[b.name] = (counts[b.name] ?? 0) + 1;
157
+ }
158
+ }
159
+ }
160
+
161
+ function csvEscape(value: unknown): string {
162
+ let text = value == null ? '' : String(value);
163
+ if (/^[=+\-@]/.test(text)) text = `'${text}`;
164
+ return `"${text.replace(/"/g, '""')}"`;
165
+ }
166
+
167
+ function formatDateForFilename(): string {
168
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
169
+ }
170
+
171
+ function parseBoolean(value: unknown): boolean {
172
+ return value === true || value === 'true' || value === '1';
173
+ }
174
+
175
+ async function* walkJsonl(dir: string): AsyncGenerator<string> {
176
+ let entries;
177
+ try {
178
+ entries = await readdir(dir, { withFileTypes: true });
179
+ } catch {
180
+ return;
181
+ }
182
+
183
+ for (const entry of entries) {
184
+ const full = path.join(dir, entry.name);
185
+ if (entry.isDirectory()) {
186
+ yield* walkJsonl(full);
187
+ } else if (
188
+ entry.isFile() &&
189
+ entry.name.endsWith('.jsonl') &&
190
+ !entry.name.startsWith('agent_')
191
+ ) {
192
+ yield full;
193
+ }
194
+ }
195
+ }
196
+
197
+ export class ConversationTelemetryService {
198
+ private readonly cc: CcConnectClient;
199
+ private readonly listTeams: () => Promise<TeamManifest[]>;
200
+ private readonly readTeamManifest: (teamName: string) => Promise<TeamManifest>;
201
+ private readonly claudeCache = new Map<string, CachedClaudeSession>();
202
+
203
+ constructor(options: ConversationTelemetryServiceOptions) {
204
+ this.cc = options.cc;
205
+ this.listTeams = options.listTeams;
206
+ this.readTeamManifest = options.readTeamManifest;
207
+ }
208
+
209
+ async getConversations(
210
+ query: ConversationTelemetryQuery = {}
211
+ ): Promise<ConversationTelemetryResponse> {
212
+ const includeContent = query.includeContent ?? 'none';
213
+ const limit = Math.min(
214
+ Math.max(Number(query.limit ?? DEFAULT_LIMIT) || DEFAULT_LIMIT, 1),
215
+ MAX_LIMIT
216
+ );
217
+ const offset = Math.max(Number(query.offset ?? 0) || 0, 0);
218
+ const platforms = String(query.platform ?? '')
219
+ .split(',')
220
+ .map((p) => p.trim().toLowerCase())
221
+ .filter(Boolean);
222
+
223
+ const [claudeIndex, projects] = await Promise.all([
224
+ this.buildClaudeIndex(),
225
+ this.resolveConversationProjects(query.teamName),
226
+ ]);
227
+ const identities = await this.readIdentities();
228
+ const rows: ConversationTelemetryRow[] = [];
229
+
230
+ // Track which claude session IDs were already matched by cc-connect
231
+ const matchedClaudeSessionIds = new Set<string>();
232
+
233
+ for (const team of projects) {
234
+ const projectName = team.bindProject || team.slug;
235
+ let sessions;
236
+ try {
237
+ sessions = await this.cc.listSessions(projectName);
238
+ } catch {
239
+ continue;
240
+ }
241
+
242
+ const deduped = this.dedupeSessions(sessions);
243
+ for (const session of deduped) {
244
+ if (
245
+ platforms.length > 0 &&
246
+ !platforms.includes(String(session.platform ?? '').toLowerCase())
247
+ ) {
248
+ continue;
249
+ }
250
+
251
+ await this.upsertIdentityRecord(identities, {
252
+ teamName: team.slug,
253
+ projectName,
254
+ platform: session.platform,
255
+ sessionKey: session.session_key,
256
+ ccSessionId: session.id,
257
+ userName: normalizeOptional(session.user_name),
258
+ chatName: normalizeOptional(session.chat_name),
259
+ firstSeenAt: session.created_at,
260
+ lastSeenAt: session.updated_at,
261
+ source: 'cc-session-name',
262
+ });
263
+
264
+ const row = await this.buildRow(team, projectName, session, identities, claudeIndex, {
265
+ includeContent,
266
+ includeToolResults: query.includeToolResults !== false,
267
+ includeSystemMessages: query.includeSystemMessages !== false,
268
+ });
269
+ if (row.session.claudeSessionId) {
270
+ matchedClaudeSessionIds.add(row.session.claudeSessionId);
271
+ }
272
+ if (!this.matchesQuery(row, query)) continue;
273
+ rows.push(row);
274
+ }
275
+ }
276
+
277
+ // Include local-only JSONL sessions not tracked by cc-connect
278
+ for (const [sessionId, summaries] of claudeIndex) {
279
+ if (matchedClaudeSessionIds.has(sessionId)) continue;
280
+ for (const summary of summaries) {
281
+ const row = this.buildLocalRow(sessionId, summary, {
282
+ includeContent,
283
+ includeToolResults: query.includeToolResults !== false,
284
+ includeSystemMessages: query.includeSystemMessages !== false,
285
+ });
286
+ if (!this.matchesQuery(row, query)) continue;
287
+ rows.push(row);
288
+ }
289
+ }
290
+
291
+ await this.writeIdentities(identities);
292
+
293
+ const isRunning = (row: ConversationTelemetryRow): boolean =>
294
+ row.session.live === true || row.session.active === true;
295
+ rows.sort((a, b) => {
296
+ const runningDiff = Number(isRunning(b)) - Number(isRunning(a));
297
+ if (runningDiff !== 0) return runningDiff;
298
+ const aTime = a.session.updatedAt ?? a.session.endTime ?? '';
299
+ const bTime = b.session.updatedAt ?? b.session.endTime ?? '';
300
+ return bTime.localeCompare(aTime);
301
+ });
302
+
303
+ const paged = rows.slice(offset, offset + limit);
304
+ const totalTokens = rows.reduce((sum, row) => sum + row.usage.totalTokens, 0);
305
+ const runningConversations = rows.filter(isRunning).length;
306
+ const missingIdentityIds = rows.filter((row) => !row.identity.id).length;
307
+ const unmatchedSessions = rows.filter((row) => row.session.matchStatus !== 'matched').length;
308
+
309
+ return {
310
+ rows: paged,
311
+ nextOffset: offset + limit < rows.length ? offset + limit : undefined,
312
+ computedAt: new Date().toISOString(),
313
+ summary: {
314
+ conversations: rows.length,
315
+ runningConversations,
316
+ missingIdentityIds,
317
+ unmatchedSessions,
318
+ totalTokens,
319
+ },
320
+ };
321
+ }
322
+
323
+ async getConversationDetail(
324
+ sessionId: string,
325
+ query: ConversationTelemetryQuery = {}
326
+ ): Promise<ConversationTelemetryRow | null> {
327
+ const response = await this.getConversations({
328
+ ...query,
329
+ includeContent: 'full',
330
+ limit: MAX_LIMIT,
331
+ offset: 0,
332
+ });
333
+ return (
334
+ response.rows.find(
335
+ (row) =>
336
+ row.session.ccSessionId === sessionId ||
337
+ row.session.sessionKey === sessionId ||
338
+ row.session.claudeSessionId === sessionId
339
+ ) ?? null
340
+ );
341
+ }
342
+
343
+ async exportConversations(
344
+ format: ConversationTelemetryExportFormat,
345
+ query: ConversationTelemetryQuery = {}
346
+ ): Promise<ConversationTelemetryExportResponse> {
347
+ const includeContent = query.includeContent ?? (format === 'csv' ? 'full' : 'full');
348
+ const response = await this.getConversations({
349
+ ...query,
350
+ includeContent,
351
+ limit: query.limit ?? MAX_LIMIT,
352
+ offset: query.offset ?? 0,
353
+ });
354
+ const stamp = formatDateForFilename();
355
+
356
+ if (format === 'json') {
357
+ return {
358
+ filename: `conversation-telemetry-${stamp}.json`,
359
+ mimeType: 'application/json;charset=utf-8',
360
+ content: JSON.stringify(response, null, 2),
361
+ };
362
+ }
363
+
364
+ if (format === 'markdown') {
365
+ return {
366
+ filename: `conversation-telemetry-${stamp}.md`,
367
+ mimeType: 'text/markdown;charset=utf-8',
368
+ content: this.toMarkdown(response.rows),
369
+ };
370
+ }
371
+
372
+ if (format === 'plaintext') {
373
+ return {
374
+ filename: `conversation-telemetry-${stamp}.txt`,
375
+ mimeType: 'text/plain;charset=utf-8',
376
+ content: this.toPlainText(response.rows),
377
+ };
378
+ }
379
+
380
+ return {
381
+ filename: `conversation-telemetry-${stamp}.csv`,
382
+ mimeType: 'text/csv;charset=utf-8',
383
+ content: this.toCsv(response.rows),
384
+ };
385
+ }
386
+
387
+ private async resolveConversationProjects(teamName?: string): Promise<ConversationProject[]> {
388
+ const localTeams = await this.listTeams().catch(() => []);
389
+ const byProject = new Map<string, ConversationProject>();
390
+
391
+ for (const team of localTeams) {
392
+ byProject.set(team.bindProject || team.slug, {
393
+ slug: team.slug,
394
+ displayName: team.displayName,
395
+ bindProject: team.bindProject || team.slug,
396
+ });
397
+ }
398
+
399
+ if (teamName?.trim()) {
400
+ const requested = teamName.trim();
401
+ try {
402
+ const team = await this.readTeamManifest(requested);
403
+ return [
404
+ {
405
+ slug: team.slug,
406
+ displayName: team.displayName,
407
+ bindProject: team.bindProject || team.slug,
408
+ },
409
+ ];
410
+ } catch {
411
+ return [
412
+ byProject.get(requested) ?? {
413
+ slug: requested,
414
+ displayName: requested,
415
+ bindProject: requested,
416
+ },
417
+ ];
418
+ }
419
+ }
420
+
421
+ try {
422
+ const ccProjects = await this.cc.listProjects();
423
+ for (const project of ccProjects) {
424
+ if (!byProject.has(project.name)) {
425
+ byProject.set(project.name, {
426
+ slug: project.name,
427
+ displayName: project.name,
428
+ bindProject: project.name,
429
+ });
430
+ }
431
+ }
432
+ } catch {
433
+ // Local team projects remain enough for a best-effort telemetry view.
434
+ }
435
+
436
+ return [...byProject.values()];
437
+ }
438
+
439
+ private dedupeSessions<
440
+ T extends {
441
+ session_key: string;
442
+ updated_at: string;
443
+ active: boolean;
444
+ live: boolean;
445
+ history_count: number;
446
+ },
447
+ >(sessions: T[]): T[] {
448
+ const byKey = new Map<string, T>();
449
+ const score = (session: T): number => {
450
+ const updatedAt = Date.parse(session.updated_at);
451
+ return (
452
+ (session.live ? 1_000_000_000_000_000 : 0) +
453
+ (session.active ? 1_000_000_000_000 : 0) +
454
+ (session.history_count ?? 0) * 1_000_000 +
455
+ (Number.isFinite(updatedAt) ? updatedAt / 1_000_000 : 0)
456
+ );
457
+ };
458
+ for (const session of sessions) {
459
+ const existing = byKey.get(session.session_key);
460
+ if (!existing || score(session) > score(existing)) byKey.set(session.session_key, session);
461
+ }
462
+ return [...byKey.values()];
463
+ }
464
+
465
+ private async buildRow(
466
+ team: ConversationProject,
467
+ projectName: string,
468
+ session: {
469
+ id: string;
470
+ name: string;
471
+ session_key: string;
472
+ active: boolean;
473
+ live: boolean;
474
+ history_count: number;
475
+ created_at: string;
476
+ updated_at: string;
477
+ last_message: { role: string; content: string; timestamp: string } | null;
478
+ platform: string;
479
+ user_name?: string;
480
+ chat_name?: string;
481
+ },
482
+ identities: Map<string, ConversationIdentityRecord>,
483
+ claudeIndex: Map<string, ClaudeSessionSummary[]>,
484
+ options: {
485
+ includeContent: 'none' | 'summary' | 'full';
486
+ includeToolResults: boolean;
487
+ includeSystemMessages: boolean;
488
+ }
489
+ ): Promise<ConversationTelemetryRow> {
490
+ const detail = await this.safeGetSessionDetail(projectName, session.id);
491
+ const agentSessionId = normalizeOptional(detail?.agent_session_id);
492
+ const matches = agentSessionId ? (claudeIndex.get(agentSessionId) ?? []) : [];
493
+ const matchStatus = !agentSessionId
494
+ ? 'missing-agent-session-id'
495
+ : matches.length === 0
496
+ ? 'jsonl-not-found'
497
+ : matches.length > 1
498
+ ? 'ambiguous'
499
+ : 'matched';
500
+ const matched = matchStatus === 'matched' ? matches[0] : undefined;
501
+ const identity = identities.get(this.identityKey(team.slug, session.session_key));
502
+ const parsedIdentity = parseSessionIdentity(session.session_key);
503
+ const platform = parsedIdentity.platform ?? session.platform;
504
+ const rawUserName = identity?.userName ?? normalizeOptional(session.user_name);
505
+ const rawChatName = identity?.chatName ?? normalizeOptional(session.chat_name);
506
+ const userId = identity?.userId ?? parsedIdentity.userId;
507
+ const chatId = identity?.chatId ?? parsedIdentity.chatId;
508
+ const chatName = rawChatName && !looksLikeChannelId(rawChatName) ? rawChatName : undefined;
509
+ const userName = rawUserName && !looksLikeChannelId(rawUserName) ? rawUserName : undefined;
510
+ const type = chatName
511
+ ? 'group'
512
+ : chatId
513
+ ? 'unknown'
514
+ : userId || userName
515
+ ? 'person'
516
+ : 'unknown';
517
+ const identityId = chatId ?? userId;
518
+ const displayName =
519
+ chatName ??
520
+ userName ??
521
+ (chatId ? formatIdentityFallback(platform, 'conversation', chatId) : undefined) ??
522
+ (userId ? formatIdentityFallback(platform, 'person', userId) : undefined) ??
523
+ session.name ??
524
+ session.session_key;
525
+
526
+ const messages = this.filterMessages(matched?.messages ?? [], options);
527
+ const userMessages = messages.filter(
528
+ (message) => message.role === 'user' && message.content.trim()
529
+ );
530
+ const firstUserMessage = userMessages[0];
531
+ const lastUserMessage = userMessages[userMessages.length - 1];
532
+ const lastMessage = [...messages].reverse().find((message) => message.content.trim());
533
+ const totalTokens = matched
534
+ ? matched.inputTokens +
535
+ matched.outputTokens +
536
+ matched.cacheReadTokens +
537
+ matched.cacheCreationTokens
538
+ : 0;
539
+
540
+ return {
541
+ teamName: team.slug,
542
+ teamDisplayName: team.displayName,
543
+ projectName,
544
+ session: {
545
+ ccSessionId: session.id,
546
+ sessionKey: session.session_key,
547
+ agentSessionId,
548
+ claudeSessionId: matched?.sessionId,
549
+ projectPath: matched?.projectPath,
550
+ jsonlRelPath: matched?.relPath,
551
+ createdAt: session.created_at,
552
+ updatedAt: session.updated_at,
553
+ startTime: matched?.startTime,
554
+ endTime: matched?.endTime,
555
+ active: session.active,
556
+ live: session.live,
557
+ matchStatus,
558
+ },
559
+ identity: {
560
+ platform,
561
+ type,
562
+ id: identityId,
563
+ userId,
564
+ chatId,
565
+ displayName,
566
+ userName,
567
+ chatName,
568
+ confidence: identityId
569
+ ? 'exact-id'
570
+ : displayName === session.session_key
571
+ ? 'session-key-only'
572
+ : 'name-only',
573
+ },
574
+ content: {
575
+ messageCount: matched?.messageCount ?? session.history_count ?? 0,
576
+ userMessageCount: matched?.userMessageCount ?? 0,
577
+ assistantMessageCount: matched?.assistantMessageCount ?? 0,
578
+ toolResultCount: matched?.toolResultCount ?? 0,
579
+ firstUserMessage: firstUserMessage?.content,
580
+ firstUserMessageAt: firstUserMessage?.timestamp,
581
+ lastUserMessage: lastUserMessage?.content,
582
+ lastUserMessageAt: lastUserMessage?.timestamp,
583
+ lastMessageRole: lastMessage?.role,
584
+ lastMessageContent: lastMessage?.content,
585
+ lastMessageAt: lastMessage?.timestamp,
586
+ text: options.includeContent === 'none' ? undefined : this.summarizeMessages(messages),
587
+ messages: options.includeContent === 'full' ? messages : undefined,
588
+ },
589
+ usage: {
590
+ inputTokens: matched?.inputTokens ?? 0,
591
+ outputTokens: matched?.outputTokens ?? 0,
592
+ cacheReadTokens: matched?.cacheReadTokens ?? 0,
593
+ cacheCreationTokens: matched?.cacheCreationTokens ?? 0,
594
+ totalTokens,
595
+ assistantTurnsWithUsage: matched?.assistantTurnsWithUsage ?? 0,
596
+ models: matched?.models ?? {},
597
+ toolCalls: matched?.toolCalls ?? {},
598
+ usageSource: matched ? 'claude-jsonl' : 'missing',
599
+ },
600
+ };
601
+ }
602
+
603
+ /** Build a row from local JSONL data only — no cc-connect session. */
604
+ private buildLocalRow(
605
+ sessionId: string,
606
+ summary: ClaudeSessionSummary,
607
+ options: {
608
+ includeContent: 'none' | 'summary' | 'full';
609
+ includeToolResults: boolean;
610
+ includeSystemMessages: boolean;
611
+ },
612
+ ): ConversationTelemetryRow {
613
+ const messages = this.filterMessages(summary.messages, options);
614
+ const userMessages = messages.filter(
615
+ (message) => message.role === 'user' && message.content.trim(),
616
+ );
617
+ const firstUserMessage = userMessages[0];
618
+ const lastUserMessage = userMessages[userMessages.length - 1];
619
+ const lastMessage = [...messages].reverse().find((message) => message.content.trim());
620
+ const totalTokens =
621
+ summary.inputTokens +
622
+ summary.outputTokens +
623
+ summary.cacheReadTokens +
624
+ summary.cacheCreationTokens;
625
+
626
+ return {
627
+ teamName: '',
628
+ teamDisplayName: '本地会话',
629
+ projectName: '',
630
+ session: {
631
+ ccSessionId: undefined,
632
+ sessionKey: sessionId,
633
+ agentSessionId: undefined,
634
+ claudeSessionId: sessionId,
635
+ projectPath: summary.projectPath,
636
+ jsonlRelPath: summary.relPath,
637
+ createdAt: summary.startTime,
638
+ updatedAt: summary.endTime,
639
+ startTime: summary.startTime,
640
+ endTime: summary.endTime,
641
+ active: false,
642
+ live: false,
643
+ matchStatus: 'local-only',
644
+ },
645
+ identity: {
646
+ platform: 'local',
647
+ type: 'person',
648
+ id: undefined,
649
+ userId: undefined,
650
+ chatId: undefined,
651
+ displayName: summary.projectPath
652
+ ? path.basename(summary.projectPath)
653
+ : sessionId.slice(0, 12),
654
+ userName: undefined,
655
+ chatName: undefined,
656
+ confidence: 'session-key-only',
657
+ },
658
+ content: {
659
+ messageCount: summary.messageCount,
660
+ userMessageCount: summary.userMessageCount,
661
+ assistantMessageCount: summary.assistantMessageCount,
662
+ toolResultCount: summary.toolResultCount,
663
+ firstUserMessage: firstUserMessage?.content,
664
+ firstUserMessageAt: firstUserMessage?.timestamp,
665
+ lastUserMessage: lastUserMessage?.content,
666
+ lastUserMessageAt: lastUserMessage?.timestamp,
667
+ lastMessageRole: lastMessage?.role,
668
+ lastMessageContent: lastMessage?.content,
669
+ lastMessageAt: lastMessage?.timestamp,
670
+ text: options.includeContent === 'none' ? undefined : this.summarizeMessages(messages),
671
+ messages: options.includeContent === 'full' ? messages : undefined,
672
+ },
673
+ usage: {
674
+ inputTokens: summary.inputTokens,
675
+ outputTokens: summary.outputTokens,
676
+ cacheReadTokens: summary.cacheReadTokens,
677
+ cacheCreationTokens: summary.cacheCreationTokens,
678
+ totalTokens,
679
+ assistantTurnsWithUsage: summary.assistantTurnsWithUsage,
680
+ models: summary.models,
681
+ toolCalls: summary.toolCalls,
682
+ usageSource: 'claude-jsonl',
683
+ },
684
+ };
685
+ }
686
+
687
+ private async safeGetSessionDetail(projectName: string, sessionId: string) {
688
+ try {
689
+ return await this.cc.getSession(projectName, sessionId, 1);
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+
695
+ private matchesQuery(row: ConversationTelemetryRow, query: ConversationTelemetryQuery): boolean {
696
+ if (query.identityType && row.identity.type !== query.identityType) return false;
697
+ if (query.identityId && row.identity.id !== query.identityId) return false;
698
+ if (query.from) {
699
+ const t = Date.parse(row.session.updatedAt ?? row.session.endTime ?? '');
700
+ if (Number.isFinite(t) && t < Date.parse(query.from)) return false;
701
+ }
702
+ if (query.to) {
703
+ const t = Date.parse(row.session.updatedAt ?? row.session.endTime ?? '');
704
+ if (Number.isFinite(t) && t > Date.parse(query.to)) return false;
705
+ }
706
+ return true;
707
+ }
708
+
709
+ private async buildClaudeIndex(): Promise<Map<string, ClaudeSessionSummary[]>> {
710
+ const root = getProjectsBasePath();
711
+ const index = new Map<string, ClaudeSessionSummary[]>();
712
+
713
+ for await (const filePath of walkJsonl(root)) {
714
+ let fileStat;
715
+ try {
716
+ fileStat = await stat(filePath);
717
+ } catch {
718
+ continue;
719
+ }
720
+
721
+ const cached = this.claudeCache.get(filePath);
722
+ const parsed =
723
+ cached && cached.size === fileStat.size && cached.mtimeMs === fileStat.mtimeMs
724
+ ? cached.parsed
725
+ : await this.parseClaudeJsonl(root, filePath);
726
+ if (!cached || cached.size !== fileStat.size || cached.mtimeMs !== fileStat.mtimeMs) {
727
+ this.claudeCache.set(filePath, { size: fileStat.size, mtimeMs: fileStat.mtimeMs, parsed });
728
+ }
729
+
730
+ const bucket = index.get(parsed.sessionId) ?? [];
731
+ bucket.push(parsed);
732
+ index.set(parsed.sessionId, bucket);
733
+ }
734
+
735
+ return index;
736
+ }
737
+
738
+ private async parseClaudeJsonl(root: string, filePath: string): Promise<ClaudeSessionSummary> {
739
+ const sessionId = path.basename(filePath, '.jsonl');
740
+ const relPath = path.relative(root, filePath);
741
+ const messages: ConversationTelemetryMessage[] = [];
742
+ const models: Record<string, number> = {};
743
+ const toolCalls: Record<string, number> = {};
744
+ let projectPath: string | undefined;
745
+ let startTime: string | undefined;
746
+ let endTime: string | undefined;
747
+ let userMessageCount = 0;
748
+ let assistantMessageCount = 0;
749
+ let toolResultCount = 0;
750
+ let inputTokens = 0;
751
+ let outputTokens = 0;
752
+ let cacheReadTokens = 0;
753
+ let cacheCreationTokens = 0;
754
+ let assistantTurnsWithUsage = 0;
755
+
756
+ const rl = createInterface({ input: createReadStream(filePath, 'utf-8'), crlfDelay: Infinity });
757
+ for await (const rawLine of rl) {
758
+ const line = rawLine.trim();
759
+ if (!line) continue;
760
+
761
+ let obj: Record<string, unknown>;
762
+ try {
763
+ obj = JSON.parse(line) as Record<string, unknown>;
764
+ } catch {
765
+ continue;
766
+ }
767
+
768
+ if (!projectPath && typeof obj.cwd === 'string') projectPath = obj.cwd;
769
+
770
+ const msg = obj.message as Record<string, unknown> | undefined;
771
+ const rawRole =
772
+ typeof msg?.role === 'string'
773
+ ? msg.role
774
+ : typeof obj.type === 'string'
775
+ ? obj.type
776
+ : undefined;
777
+ if (!rawRole || !['user', 'assistant', 'system'].includes(rawRole)) continue;
778
+
779
+ const content = msg && 'content' in msg ? msg.content : obj.content;
780
+ const isToolResultMessage =
781
+ rawRole === 'user' &&
782
+ Array.isArray(content) &&
783
+ content.some(
784
+ (block) =>
785
+ block &&
786
+ typeof block === 'object' &&
787
+ (block as Record<string, unknown>).type === 'tool_result'
788
+ );
789
+ const role: ConversationTelemetryMessage['role'] = isToolResultMessage
790
+ ? 'tool'
791
+ : (rawRole as ConversationTelemetryMessage['role']);
792
+ const timestamp = normalizeOptional(obj.timestamp) ?? normalizeOptional(msg?.timestamp);
793
+ const usage = msg?.usage as Record<string, unknown> | undefined;
794
+ const model = normalizeOptional(msg?.model);
795
+ const text = extractText(content);
796
+
797
+ if (role === 'user') userMessageCount++;
798
+ if (role === 'assistant') {
799
+ assistantMessageCount++;
800
+ countToolCalls(content, toolCalls);
801
+ if (model) models[model] = (models[model] ?? 0) + 1;
802
+ }
803
+ if (Array.isArray(content)) {
804
+ toolResultCount += content.filter(
805
+ (block) =>
806
+ block &&
807
+ typeof block === 'object' &&
808
+ (block as Record<string, unknown>).type === 'tool_result'
809
+ ).length;
810
+ }
811
+
812
+ let messageUsage: ConversationTelemetryMessage['usage'];
813
+ if (role === 'assistant' && usage) {
814
+ const input = toNumber(usage.input_tokens ?? usage.prompt_tokens);
815
+ const output = toNumber(usage.output_tokens ?? usage.completion_tokens);
816
+ const cacheRead = toNumber(
817
+ usage.cache_read_input_tokens ??
818
+ (usage.input_tokens_details as Record<string, unknown> | undefined)?.cached_tokens ??
819
+ (usage.prompt_tokens_details as Record<string, unknown> | undefined)?.cached_tokens
820
+ );
821
+ const cacheCreation = toNumber(usage.cache_creation_input_tokens);
822
+ messageUsage = {
823
+ inputTokens: input,
824
+ outputTokens: output,
825
+ cacheReadTokens: cacheRead,
826
+ cacheCreationTokens: cacheCreation,
827
+ totalTokens: input + output + cacheRead + cacheCreation,
828
+ };
829
+ inputTokens += input;
830
+ outputTokens += output;
831
+ cacheReadTokens += cacheRead;
832
+ cacheCreationTokens += cacheCreation;
833
+ assistantTurnsWithUsage++;
834
+ }
835
+
836
+ if (timestamp) {
837
+ if (!startTime) startTime = timestamp;
838
+ endTime = timestamp;
839
+ }
840
+
841
+ messages.push({
842
+ role: role as ConversationTelemetryMessage['role'],
843
+ timestamp,
844
+ content: text,
845
+ uuid: normalizeOptional(obj.uuid),
846
+ parentUuid: normalizeOptional(obj.parentUuid) ?? null,
847
+ model,
848
+ requestId: normalizeOptional(obj.requestId),
849
+ usage: messageUsage,
850
+ isMeta: obj.isMeta === true,
851
+ });
852
+ }
853
+
854
+ return {
855
+ sessionId,
856
+ relPath,
857
+ filePath,
858
+ projectPath,
859
+ startTime,
860
+ endTime,
861
+ messageCount: messages.length,
862
+ userMessageCount,
863
+ assistantMessageCount,
864
+ toolResultCount,
865
+ inputTokens,
866
+ outputTokens,
867
+ cacheReadTokens,
868
+ cacheCreationTokens,
869
+ assistantTurnsWithUsage,
870
+ models,
871
+ toolCalls,
872
+ messages,
873
+ };
874
+ }
875
+
876
+ private filterMessages(
877
+ messages: ConversationTelemetryMessage[],
878
+ options: { includeToolResults: boolean; includeSystemMessages: boolean }
879
+ ): ConversationTelemetryMessage[] {
880
+ return messages.filter((message) => {
881
+ if (!options.includeSystemMessages && message.role === 'system') return false;
882
+ if (!options.includeToolResults && message.role === 'tool') return false;
883
+ return true;
884
+ });
885
+ }
886
+
887
+ private summarizeMessages(messages: ConversationTelemetryMessage[]): string {
888
+ return messages
889
+ .filter((message) => message.content.trim())
890
+ .slice(0, 20)
891
+ .map((message) => `[${message.timestamp ?? '-'}] ${message.role}: ${message.content}`)
892
+ .join('\n\n');
893
+ }
894
+
895
+ private identityKey(teamName: string, sessionKey: string): string {
896
+ return `${teamName}\0${sessionKey}`;
897
+ }
898
+
899
+ private async readIdentities(): Promise<Map<string, ConversationIdentityRecord>> {
900
+ try {
901
+ const raw = await readFile(identityStorePath(), 'utf-8');
902
+ const parsed = JSON.parse(raw) as { records?: ConversationIdentityRecord[] };
903
+ const records = Array.isArray(parsed.records) ? parsed.records : [];
904
+ return new Map(
905
+ records.map((record) => [this.identityKey(record.teamName, record.sessionKey), record])
906
+ );
907
+ } catch {
908
+ return new Map();
909
+ }
910
+ }
911
+
912
+ private async writeIdentities(records: Map<string, ConversationIdentityRecord>): Promise<void> {
913
+ const filePath = identityStorePath();
914
+ await mkdir(path.dirname(filePath), { recursive: true });
915
+ const tmp = `${filePath}.${process.pid}.tmp`;
916
+ await writeFile(
917
+ tmp,
918
+ JSON.stringify({ schemaVersion: 1, records: [...records.values()] }, null, 2),
919
+ 'utf-8'
920
+ );
921
+ await rename(tmp, filePath);
922
+ }
923
+
924
+ private async upsertIdentityRecord(
925
+ records: Map<string, ConversationIdentityRecord>,
926
+ next: ConversationIdentityRecord
927
+ ): Promise<void> {
928
+ const key = this.identityKey(next.teamName, next.sessionKey);
929
+ const existing = records.get(key);
930
+ records.set(key, {
931
+ ...existing,
932
+ ...next,
933
+ userName: next.userName ?? existing?.userName,
934
+ chatName: next.chatName ?? existing?.chatName,
935
+ firstSeenAt: existing?.firstSeenAt ?? next.firstSeenAt,
936
+ lastSeenAt: next.lastSeenAt || existing?.lastSeenAt || new Date().toISOString(),
937
+ });
938
+ }
939
+
940
+ private toCsv(rows: ConversationTelemetryRow[]): string {
941
+ const headers = [
942
+ 'teamName',
943
+ 'teamDisplayName',
944
+ 'projectName',
945
+ 'ccSessionId',
946
+ 'sessionName',
947
+ 'sessionKey',
948
+ 'agentSessionId',
949
+ 'claudeSessionId',
950
+ 'platform',
951
+ 'identityType',
952
+ 'identityId',
953
+ 'displayName',
954
+ 'userName',
955
+ 'chatName',
956
+ 'messageRole',
957
+ 'messageTimestamp',
958
+ 'messageContent',
959
+ 'inputTokens',
960
+ 'outputTokens',
961
+ 'cacheReadTokens',
962
+ 'cacheCreationTokens',
963
+ 'totalTokens',
964
+ 'usageSource',
965
+ 'matchStatus',
966
+ 'createdAt',
967
+ 'updatedAt',
968
+ ];
969
+
970
+ const lines = [headers.map(csvEscape).join(',')];
971
+ for (const row of rows) {
972
+ const messages = row.content.messages?.length
973
+ ? row.content.messages.filter((message) => message.content.trim())
974
+ : [
975
+ {
976
+ role: row.content.lastMessageRole ?? 'unknown',
977
+ timestamp: row.content.lastMessageAt,
978
+ content:
979
+ row.content.lastMessageContent ??
980
+ row.content.lastUserMessage ??
981
+ row.content.firstUserMessage ??
982
+ '',
983
+ usage: undefined,
984
+ } satisfies ConversationTelemetryMessage,
985
+ ];
986
+
987
+ for (const message of messages) {
988
+ lines.push(
989
+ [
990
+ row.teamName,
991
+ row.teamDisplayName,
992
+ row.projectName,
993
+ row.session.ccSessionId,
994
+ row.identity.displayName,
995
+ row.session.sessionKey,
996
+ row.session.agentSessionId,
997
+ row.session.claudeSessionId,
998
+ row.identity.platform,
999
+ row.identity.type,
1000
+ row.identity.id,
1001
+ row.identity.displayName,
1002
+ row.identity.userName,
1003
+ row.identity.chatName,
1004
+ message.role,
1005
+ message.timestamp,
1006
+ message.content,
1007
+ message.usage?.inputTokens ?? '',
1008
+ message.usage?.outputTokens ?? '',
1009
+ message.usage?.cacheReadTokens ?? '',
1010
+ message.usage?.cacheCreationTokens ?? '',
1011
+ message.usage?.totalTokens ?? '',
1012
+ row.usage.usageSource,
1013
+ row.session.matchStatus,
1014
+ row.session.createdAt,
1015
+ row.session.updatedAt,
1016
+ ]
1017
+ .map(csvEscape)
1018
+ .join(',')
1019
+ );
1020
+ }
1021
+ }
1022
+ return `${lines.join('\n')}\n`;
1023
+ }
1024
+
1025
+ private toMarkdown(rows: ConversationTelemetryRow[]): string {
1026
+ const parts = ['# Conversation Telemetry Export', ''];
1027
+ for (const row of rows) {
1028
+ parts.push(`## ${row.identity.displayName}`);
1029
+ parts.push('');
1030
+ parts.push(`- Team: ${row.teamDisplayName} (${row.teamName})`);
1031
+ parts.push(`- Session: ${row.session.sessionKey}`);
1032
+ parts.push(`- Platform: ${row.identity.platform}`);
1033
+ parts.push(`- Identity: ${row.identity.type} / ${row.identity.confidence}`);
1034
+ if (row.identity.userName) parts.push(`- User: ${row.identity.userName}`);
1035
+ if (row.identity.chatName) parts.push(`- Chat: ${row.identity.chatName}`);
1036
+ parts.push(`- Match: ${row.session.matchStatus}`);
1037
+ parts.push(
1038
+ `- Tokens: total ${row.usage.totalTokens} / input ${row.usage.inputTokens} / output ${row.usage.outputTokens} / cache read ${row.usage.cacheReadTokens} / cache creation ${row.usage.cacheCreationTokens}`
1039
+ );
1040
+ if (row.content.firstUserMessage) {
1041
+ parts.push(`- First question: ${row.content.firstUserMessage}`);
1042
+ }
1043
+ if (
1044
+ row.content.lastUserMessage &&
1045
+ row.content.lastUserMessage !== row.content.firstUserMessage
1046
+ ) {
1047
+ parts.push(`- Last question: ${row.content.lastUserMessage}`);
1048
+ }
1049
+ parts.push('');
1050
+ if (row.content.messages?.length) {
1051
+ parts.push('### Transcript', '');
1052
+ for (const message of row.content.messages) {
1053
+ if (!message.content.trim()) continue;
1054
+ parts.push(`**${message.role}** ${message.timestamp ?? ''}`.trim());
1055
+ parts.push('');
1056
+ parts.push(message.content);
1057
+ parts.push('');
1058
+ }
1059
+ }
1060
+ }
1061
+ return parts.join('\n');
1062
+ }
1063
+
1064
+ private toPlainText(rows: ConversationTelemetryRow[]): string {
1065
+ return rows
1066
+ .map((row) => {
1067
+ const header = [
1068
+ `Conversation: ${row.identity.displayName}`,
1069
+ `Team: ${row.teamDisplayName} (${row.teamName})`,
1070
+ `Session: ${row.session.sessionKey}`,
1071
+ `Platform: ${row.identity.platform}`,
1072
+ row.identity.userName ? `User: ${row.identity.userName}` : undefined,
1073
+ row.identity.chatName ? `Chat: ${row.identity.chatName}` : undefined,
1074
+ `Match: ${row.session.matchStatus}`,
1075
+ `Tokens: total ${row.usage.totalTokens} / input ${row.usage.inputTokens} / output ${row.usage.outputTokens} / cache read ${row.usage.cacheReadTokens} / cache creation ${row.usage.cacheCreationTokens}`,
1076
+ row.content.firstUserMessage
1077
+ ? `First question: ${row.content.firstUserMessage}`
1078
+ : undefined,
1079
+ row.content.lastUserMessage &&
1080
+ row.content.lastUserMessage !== row.content.firstUserMessage
1081
+ ? `Last question: ${row.content.lastUserMessage}`
1082
+ : undefined,
1083
+ ]
1084
+ .filter((line): line is string => Boolean(line))
1085
+ .join('\n');
1086
+ const transcript =
1087
+ row.content.messages
1088
+ ?.filter((message) => message.content.trim())
1089
+ .map((message) => `[${message.timestamp ?? '-'}] ${message.role}\n${message.content}`)
1090
+ .join('\n\n') ?? '';
1091
+ return `${header}\n\n${transcript}`.trim();
1092
+ })
1093
+ .join('\n\n---\n\n');
1094
+ }
1095
+ }
1096
+
1097
+ export function shouldIncludeContent(value: unknown): 'none' | 'summary' | 'full' {
1098
+ if (value === 'full' || value === 'summary') return value;
1099
+ if (parseBoolean(value)) return 'full';
1100
+ return 'none';
1101
+ }